diff --git a/.gitignore b/.gitignore index d4063a95..5776cd0c 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ /.fleet /.vscode/*.log /cmd/answer/*.sh +/cmd/answer/answer /cmd/answer/uploads/* /cmd/logs /configs/config-dev.yaml @@ -22,3 +23,5 @@ tmp vendor/ /answer-data/ /answer + +dist/ diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index cc4dd08e..f96e65ef 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -13,8 +13,6 @@ stages: "compile the html and other static files": image: node:16 stage: compile-html - tags: - - runner-nanjing before_script: - npm config set registry https://repo.huaweicloud.com/repository/npm/ - make install-ui-packages @@ -27,10 +25,9 @@ stages: "compile the golang project": image: golang:1.18 stage: compile-golang - before_script: - - export GOPROXY=https://goproxy.cn,direct +# before_script: +# - export GOPROXY=https://goproxy.cn,direct script: - - make generate - make build artifacts: paths: @@ -38,8 +35,8 @@ stages: "build docker images and push": stage: push - before_script: - - export GOPROXY=https://goproxy.cn,direct +# before_script: +# - export GOPROXY=https://goproxy.cn,direct extends: .docker-build-push only: - test diff --git a/.goreleaser.yaml b/.goreleaser.yaml new file mode 100644 index 00000000..fd9380ee --- /dev/null +++ b/.goreleaser.yaml @@ -0,0 +1,90 @@ +env: + - GO11MODULE=on + - GO111MODULE=on + - GOPROXY=https://goproxy.io + - CGO_ENABLED=1 + +before: + hooks: + - go mod tidy +builds: + - id: build-amd64 + main: ./cmd/answer/. + binary: answer + ldflags: -s -w -X main.Version={{.Version}} -X main.Revision={{.ShortCommit}} -X main.Time={{.Date}} -X main.BuildUser=goreleaser + goos: + - linux + goarch: + - amd64 + # linux windows need cgomingw64-gcc + - id: build-windows + main: ./cmd/answer/. + binary: answer + ldflags: -s -w -X main.Version={{.Version}} -X main.Revision={{.ShortCommit}} -X main.Time={{.Date}} -X main.BuildUser=goreleaser + env: + - CC=x86_64-w64-mingw32-gcc + - CXX=x86_64-w64-mingw32-g++ + goos: + - windows + goarch: + - amd64 + # linux arm64 need cgo arm64 + - id: build-arm64 + main: ./cmd/answer/. + binary: answer + ldflags: -s -w -X main.Version={{.Version}} -X main.Revision={{.ShortCommit}} -X main.Time={{.Date}} -X main.BuildUser=goreleaser + env: + - CC=aarch64-linux-gnu-gcc + - CXX=aarch64-linux-gnu-g++ + goos: + - linux + goarch: + - arm64 + - id: build-darwin-arm64 + main: ./cmd/answer/. + binary: answer + env: + - CC=oa64-clang + - CXX=oa64-clang++ + goos: + - darwin + goarch: + - arm64 + ldflags: -s -w -X main.Version={{.Version}} -X main.Revision={{.ShortCommit}} -X main.Time={{.Date}} -X main.BuildUser=goreleaser + flags: -v + - id: build-darwin-amd64 + main: ./cmd/answer/. + binary: answer + env: + - CC=o64-clang + - CXX=o64-clang++ + goos: + - darwin + goarch: + - amd64 + ldflags: -s -w -X main.Version={{.Version}} -X main.Revision={{.ShortCommit}} -X main.Time={{.Date}} -X main.BuildUser=goreleaser + flags: -v + + +archives: + - replacements: + darwin: Darwin + amd64: x86_64 +checksum: + name_template: 'checksums.txt' +snapshot: + name_template: "{{ incpatch .Version }}" +changelog: + sort: asc + filters: + exclude: + - '^docs:' + - '^test:' + + +# sudo apt-get install build-essential +# sudo apt-get install gcc-multilib g++-multilib +# sudo apt-get install gcc-mingw-w64 +# sudo apt-get -y install gcc-aarch64-linux-gnu gcc-arm-linux-gnueabihf +# sudo apt-get install clang llvm +# goreleaser release --snapshot --rm-dist \ No newline at end of file diff --git a/Makefile b/Makefile index 3c0b6a40..7f4e6ec7 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ .PHONY: build clean ui -VERSION=0.5.0 +VERSION=1.0.0 BIN=answer DIR_SRC=./cmd/answer DOCKER_CMD=docker @@ -22,6 +22,7 @@ universal: generate: go get github.com/google/wire/cmd/wire@latest + go install github.com/golang/mock/mockgen@v1.6.0 go generate ./... go mod tidy diff --git a/cmd/answer/main.go b/cmd/answer/main.go index cea99db9..626eb2b2 100644 --- a/cmd/answer/main.go +++ b/cmd/answer/main.go @@ -6,6 +6,7 @@ import ( "github.com/answerdev/answer/internal/base/conf" "github.com/answerdev/answer/internal/base/constant" + "github.com/answerdev/answer/internal/base/cron" "github.com/answerdev/answer/internal/cli" "github.com/answerdev/answer/internal/schema" "github.com/gin-gonic/gin" @@ -45,6 +46,7 @@ func runApp() { if err != nil { panic(err) } + conf.GetPathIgnoreList() app, cleanup, err := initApplication( c.Debug, c.Server, c.Data.Database, c.Data.Cache, c.I18n, c.Swaggerui, c.ServiceConfig, log.GetLogger()) if err != nil { @@ -59,7 +61,8 @@ func runApp() { } } -func newApplication(serverConf *conf.Server, server *gin.Engine) *pacman.Application { +func newApplication(serverConf *conf.Server, server *gin.Engine, manager *cron.ScheduledTaskManager) *pacman.Application { + manager.Run() return pacman.NewApp( pacman.WithName(Name), pacman.WithVersion(Version), diff --git a/cmd/answer/wire.go b/cmd/answer/wire.go index 067314f7..11d735aa 100644 --- a/cmd/answer/wire.go +++ b/cmd/answer/wire.go @@ -7,11 +7,13 @@ package main import ( "github.com/answerdev/answer/internal/base/conf" + "github.com/answerdev/answer/internal/base/cron" "github.com/answerdev/answer/internal/base/data" "github.com/answerdev/answer/internal/base/middleware" "github.com/answerdev/answer/internal/base/server" "github.com/answerdev/answer/internal/base/translator" "github.com/answerdev/answer/internal/controller" + "github.com/answerdev/answer/internal/controller/template_render" "github.com/answerdev/answer/internal/controller_backyard" "github.com/answerdev/answer/internal/repo" "github.com/answerdev/answer/internal/router" @@ -37,7 +39,9 @@ func initApplication( router.ProviderSetRouter, controller.ProviderSetController, controller_backyard.ProviderSetController, + templaterender.ProviderSetTemplateRenderController, service.ProviderSetService, + cron.ProviderSetService, repo.ProviderSetRepo, translator.ProviderSet, middleware.ProviderSetMiddleware, diff --git a/cmd/answer/wire_gen.go b/cmd/answer/wire_gen.go index 852fd015..3fa06bf7 100644 --- a/cmd/answer/wire_gen.go +++ b/cmd/answer/wire_gen.go @@ -8,11 +8,13 @@ package main import ( "github.com/answerdev/answer/internal/base/conf" + "github.com/answerdev/answer/internal/base/cron" "github.com/answerdev/answer/internal/base/data" "github.com/answerdev/answer/internal/base/middleware" "github.com/answerdev/answer/internal/base/server" "github.com/answerdev/answer/internal/base/translator" "github.com/answerdev/answer/internal/controller" + "github.com/answerdev/answer/internal/controller/template_render" "github.com/answerdev/answer/internal/controller_backyard" "github.com/answerdev/answer/internal/repo/activity" "github.com/answerdev/answer/internal/repo/activity_common" @@ -31,6 +33,7 @@ import ( "github.com/answerdev/answer/internal/repo/reason" "github.com/answerdev/answer/internal/repo/report" "github.com/answerdev/answer/internal/repo/revision" + "github.com/answerdev/answer/internal/repo/role" "github.com/answerdev/answer/internal/repo/search_common" "github.com/answerdev/answer/internal/repo/site_info" "github.com/answerdev/answer/internal/repo/tag" @@ -61,6 +64,7 @@ import ( "github.com/answerdev/answer/internal/service/report_backyard" "github.com/answerdev/answer/internal/service/report_handle_backyard" "github.com/answerdev/answer/internal/service/revision_common" + role2 "github.com/answerdev/answer/internal/service/role" "github.com/answerdev/answer/internal/service/search_parser" "github.com/answerdev/answer/internal/service/service_config" "github.com/answerdev/answer/internal/service/siteinfo" @@ -109,14 +113,18 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database, userActiveActivityRepo := activity.NewUserActiveActivityRepo(dataData, activityRepo, userRankRepo, configRepo) emailRepo := export.NewEmailRepo(dataData) emailService := export2.NewEmailService(configRepo, emailRepo, siteInfoRepo) - userService := service.NewUserService(userRepo, userActiveActivityRepo, emailService, authService, serviceConf, siteInfoCommonService) + userRoleRelRepo := role.NewUserRoleRelRepo(dataData) + roleRepo := role.NewRoleRepo(dataData) + roleService := role2.NewRoleService(roleRepo) + userRoleRelService := role2.NewUserRoleRelService(userRoleRelRepo, roleService) + userCommon := usercommon.NewUserCommon(userRepo) + userService := service.NewUserService(userRepo, userActiveActivityRepo, activityRepo, emailService, authService, serviceConf, siteInfoCommonService, userRoleRelService, userCommon) captchaRepo := captcha.NewCaptchaRepo(dataData) captchaService := action.NewCaptchaService(captchaRepo) uploaderService := uploader.NewUploaderService(serviceConf, siteInfoCommonService) - userController := controller.NewUserController(authService, userService, captchaService, emailService, uploaderService) + userController := controller.NewUserController(authService, userService, captchaService, emailService, uploaderService, siteInfoCommonService) commentRepo := comment.NewCommentRepo(dataData, uniqueIDRepo) commentCommonRepo := comment.NewCommentCommonRepo(dataData, uniqueIDRepo) - userCommon := usercommon.NewUserCommon(userRepo) answerRepo := answer.NewAnswerRepo(dataData, uniqueIDRepo, userRankRepo, activityRepo) questionRepo := question.NewQuestionRepo(dataData, uniqueIDRepo) tagCommonRepo := tag_common.NewTagCommonRepo(dataData, uniqueIDRepo) @@ -128,7 +136,9 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database, objService := object_info.NewObjService(answerRepo, questionRepo, commentCommonRepo, tagCommonRepo, tagCommonService) voteRepo := activity_common.NewVoteRepo(dataData, activityRepo) commentService := comment2.NewCommentService(commentRepo, commentCommonRepo, userCommon, objService, voteRepo) - rankService := rank2.NewRankService(userCommon, userRankRepo, objService, configRepo) + rolePowerRelRepo := role.NewRolePowerRelRepo(dataData) + rolePowerRelService := role2.NewRolePowerRelService(rolePowerRelRepo, userRoleRelService) + rankService := rank2.NewRankService(userCommon, userRankRepo, objService, userRoleRelService, rolePowerRelService, configRepo) commentController := controller.NewCommentController(commentService, rankService) reportRepo := report.NewReportRepo(dataData, uniqueIDRepo) reportService := report2.NewReportService(reportRepo, objService) @@ -154,7 +164,7 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database, answerActivityRepo := activity.NewAnswerActivityRepo(dataData, activityRepo, userRankRepo) questionActivityRepo := activity.NewQuestionActivityRepo(dataData, activityRepo, userRankRepo) answerActivityService := activity2.NewAnswerActivityService(answerActivityRepo, questionActivityRepo) - questionService := service.NewQuestionService(questionRepo, tagCommonService, questionCommon, userCommon, revisionService, metaService, collectionCommon, answerActivityService) + questionService := service.NewQuestionService(questionRepo, tagCommonService, questionCommon, userCommon, revisionService, metaService, collectionCommon, answerActivityService, dataData) questionController := controller.NewQuestionController(questionService, rankService) answerService := service.NewAnswerService(answerRepo, questionRepo, questionCommon, userCommon, collectionCommon, userRepo, revisionService, answerActivityService, answerCommon, voteRepo) dashboardService := dashboard.NewDashboardService(questionRepo, answerRepo, commentCommonRepo, voteRepo, userRepo, reportRepo, configRepo, siteInfoCommonService, serviceConf, dataData) @@ -171,13 +181,13 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database, reportBackyardService := report_backyard.NewReportBackyardService(reportRepo, userCommon, commonRepo, answerRepo, questionRepo, commentCommonRepo, reportHandle, configRepo) controller_backyardReportController := controller_backyard.NewReportController(reportBackyardService) userBackyardRepo := user.NewUserBackyardRepo(dataData, authRepo) - userBackyardService := user_backyard.NewUserBackyardService(userBackyardRepo) + userBackyardService := user_backyard.NewUserBackyardService(userBackyardRepo, userRoleRelService, authService, userCommon) userBackyardController := controller_backyard.NewUserBackyardController(userBackyardService) reasonRepo := reason.NewReasonRepo(configRepo) reasonService := reason2.NewReasonService(reasonRepo) reasonController := controller.NewReasonController(reasonService) themeController := controller_backyard.NewThemeController() - siteInfoService := siteinfo.NewSiteInfoService(siteInfoRepo, emailService, tagCommonService) + siteInfoService := siteinfo.NewSiteInfoService(siteInfoRepo, siteInfoCommonService, emailService, tagCommonService) siteInfoController := controller_backyard.NewSiteInfoController(siteInfoService) siteinfoController := controller.NewSiteinfoController(siteInfoCommonService) notificationRepo := notification.NewNotificationRepo(dataData) @@ -191,13 +201,18 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database, commentCommonService := comment_common.NewCommentCommonService(commentCommonRepo) activityService := activity2.NewActivityService(activityActivityRepo, userCommon, activityCommon, tagCommonService, objService, commentCommonService, revisionService, metaService) activityController := controller.NewActivityController(activityCommon, activityService) - answerAPIRouter := router.NewAnswerAPIRouter(langController, userController, commentController, reportController, voteController, tagController, followController, collectionController, questionController, answerController, searchController, revisionController, rankController, controller_backyardReportController, userBackyardController, reasonController, themeController, siteInfoController, siteinfoController, notificationController, dashboardController, uploadController, activityController) + roleController := controller_backyard.NewRoleController(roleService) + answerAPIRouter := router.NewAnswerAPIRouter(langController, userController, commentController, reportController, voteController, tagController, followController, collectionController, questionController, answerController, searchController, revisionController, rankController, controller_backyardReportController, userBackyardController, reasonController, themeController, siteInfoController, siteinfoController, notificationController, dashboardController, uploadController, activityController, roleController) swaggerRouter := router.NewSwaggerRouter(swaggerConf) - uiRouter := router.NewUIRouter() - authUserMiddleware := middleware.NewAuthUserMiddleware(authService) + uiRouter := router.NewUIRouter(siteinfoController, siteInfoCommonService) + authUserMiddleware := middleware.NewAuthUserMiddleware(authService, siteInfoCommonService) avatarMiddleware := middleware.NewAvatarMiddleware(serviceConf, uploaderService) - ginEngine := server.NewHTTPServer(debug, staticRouter, answerAPIRouter, swaggerRouter, uiRouter, authUserMiddleware, avatarMiddleware) - application := newApplication(serverConf, ginEngine) + templateRenderController := templaterender.NewTemplateRenderController(questionService, userService, tagService, answerService, commentService, dataData, siteInfoCommonService) + templateController := controller.NewTemplateController(templateRenderController, siteInfoCommonService) + templateRouter := router.NewTemplateRouter(templateController, templateRenderController, siteInfoController) + ginEngine := server.NewHTTPServer(debug, staticRouter, answerAPIRouter, swaggerRouter, uiRouter, authUserMiddleware, avatarMiddleware, templateRouter) + scheduledTaskManager := cron.NewScheduledTaskManager(siteInfoCommonService, questionService) + application := newApplication(serverConf, ginEngine, scheduledTaskManager) return application, func() { cleanup2() cleanup() diff --git a/configs/config.go b/configs/config.go index 52c7b843..3a7c32c2 100644 --- a/configs/config.go +++ b/configs/config.go @@ -4,3 +4,6 @@ import _ "embed" //go:embed config.yaml var Config []byte + +//go:embed path_ignore.yaml +var PathIgnore []byte diff --git a/configs/path_ignore.yaml b/configs/path_ignore.yaml new file mode 100644 index 00000000..4ecb1237 --- /dev/null +++ b/configs/path_ignore.yaml @@ -0,0 +1,13 @@ +# url path reserves the keywords list +users: + - settings + - login + - register + - account-recovery + - change-email + - password-reset + - account-activation + - confirm-new-email + - account-suspended +questions: + - ask diff --git a/docs/docs.go b/docs/docs.go index 21f184bc..05b8d73a 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -105,7 +105,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/entity.AdminSetAnswerStatusRequest" + "$ref": "#/definitions/schema.AdminSetAnswerStatusRequest" } } ], @@ -432,6 +432,41 @@ const docTemplate = `{ } } }, + "/answer/admin/api/roles": { + "get": { + "description": "get role list", + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "get role list", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.GetRoleResp" + } + } + } + } + ] + } + } + } + } + }, "/answer/admin/api/setting/smtp": { "get": { "security": [ @@ -574,6 +609,77 @@ const docTemplate = `{ } } }, + "/answer/admin/api/siteinfo/custom-css-html": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "get site info custom html css config", + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "get site info custom html css config", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/schema.SiteCustomCssHTMLResp" + } + } + } + ] + } + } + } + }, + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "update site custom css html config", + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "update site custom css html config", + "parameters": [ + { + "description": "login info", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.SiteCustomCssHTMLReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, "/answer/admin/api/siteinfo/general": { "get": { "security": [ @@ -787,6 +893,219 @@ const docTemplate = `{ } } }, + "/answer/admin/api/siteinfo/login": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "get site info login config", + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "get site info login config", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/schema.SiteLoginResp" + } + } + } + ] + } + } + } + }, + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "update site login", + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "update site login", + "parameters": [ + { + "description": "login info", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.SiteLoginReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, + "/answer/admin/api/siteinfo/seo": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "get site seo information", + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "get site seo information", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/schema.SiteSeoResp" + } + } + } + ] + } + } + } + }, + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "update site seo information", + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "update site seo information", + "parameters": [ + { + "description": "seo", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.SiteSeoReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, + "/answer/admin/api/siteinfo/theme": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "get site info theme config", + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "get site info theme config", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/schema.SiteThemeResp" + } + } + } + ] + } + } + } + }, + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "update site custom css html config", + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "update site custom css html config", + "parameters": [ + { + "description": "login info", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.SiteThemeReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, "/answer/admin/api/siteinfo/write": { "get": { "security": [ @@ -883,6 +1202,123 @@ const docTemplate = `{ } } }, + "/answer/admin/api/user": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "add user", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "add user", + "parameters": [ + { + "description": "user", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.AddUserReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, + "/answer/admin/api/user/password": { + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "update user password", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "update user password", + "parameters": [ + { + "description": "user", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.UpdateUserPasswordReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, + "/answer/admin/api/user/role": { + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "update user role", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "update user role", + "parameters": [ + { + "description": "user", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.UpdateUserRoleReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, "/answer/admin/api/user/status": { "put": { "security": [ @@ -956,6 +1392,12 @@ const docTemplate = `{ "name": "query", "in": "query" }, + { + "type": "boolean", + "description": "staff user", + "name": "staff", + "in": "query" + }, { "enum": [ "suspended", @@ -2666,6 +3108,45 @@ const docTemplate = `{ } } }, + "/answer/api/v1/question/reopen": { + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "reopen question", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "api-question" + ], + "summary": "reopen question", + "parameters": [ + { + "description": "question", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.ReopenQuestionReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, "/answer/api/v1/question/search": { "post": { "description": "SearchQuestionList", @@ -3266,7 +3747,7 @@ const docTemplate = `{ "type": "object", "properties": { "data": { - "$ref": "#/definitions/schema.SiteGeneralResp" + "$ref": "#/definitions/schema.SiteInfoResp" } } } @@ -4196,6 +4677,81 @@ const docTemplate = `{ } } }, + "/answer/api/v1/user/ranking": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "get user ranking", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "get user ranking", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/schema.UserRankingResp" + } + } + } + ] + } + } + } + } + }, + "/answer/api/v1/user/register/captcha": { + "get": { + "description": "UserRegisterCaptcha", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "UserRegisterCaptcha", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/schema.GetUserResp" + } + } + } + ] + } + } + } + } + }, "/answer/api/v1/user/register/email": { "post": { "description": "UserRegisterByEmail", @@ -4344,6 +4900,26 @@ const docTemplate = `{ } } }, + "/custom.css": { + "get": { + "description": "get site robots information", + "produces": [ + "application/json" + ], + "tags": [ + "site" + ], + "summary": "get site robots information", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "string" + } + } + } + } + }, "/installation/base-info": { "post": { "description": "init base info", @@ -4592,20 +5168,29 @@ const docTemplate = `{ } } } + }, + "/robots.txt": { + "get": { + "description": "get site robots information", + "produces": [ + "application/json" + ], + "tags": [ + "site" + ], + "summary": "get site robots information", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "string" + } + } + } + } } }, "definitions": { - "entity.AdminSetAnswerStatusRequest": { - "type": "object", - "properties": { - "answer_id": { - "type": "string" - }, - "status": { - "type": "string" - } - } - }, "handler.RespBody": { "type": "object", "properties": { @@ -4732,6 +5317,9 @@ const docTemplate = `{ "display_name": { "type": "string" }, + "main_tag_slug_name": { + "type": "string" + }, "object_type": { "type": "string" }, @@ -4767,6 +5355,9 @@ const docTemplate = `{ "created_at": { "type": "integer" }, + "id": { + "type": "string" + }, "object_id": { "type": "string" }, @@ -4854,6 +5445,40 @@ const docTemplate = `{ } } }, + "schema.AddUserReq": { + "type": "object", + "required": [ + "display_name", + "email", + "password" + ], + "properties": { + "display_name": { + "type": "string", + "maxLength": 30 + }, + "email": { + "type": "string", + "maxLength": 500 + }, + "password": { + "type": "string", + "maxLength": 32, + "minLength": 8 + } + } + }, + "schema.AdminSetAnswerStatusRequest": { + "type": "object", + "properties": { + "answer_id": { + "type": "string" + }, + "status": { + "type": "string" + } + } + }, "schema.AdminSetQuestionStatusRequest": { "type": "object", "properties": { @@ -4889,7 +5514,6 @@ const docTemplate = `{ "type": "string" }, "question_id": { - "description": "question_id", "type": "string" } } @@ -5386,6 +6010,20 @@ const docTemplate = `{ } } }, + "schema.GetRoleResp": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + } + } + }, "schema.GetSMTPConfigResp": { "type": "object", "properties": { @@ -5495,6 +6133,10 @@ const docTemplate = `{ "description": "created time", "type": "integer" }, + "description": { + "description": "description text", + "type": "string" + }, "display_name": { "description": "display name", "type": "string" @@ -5614,6 +6256,14 @@ const docTemplate = `{ "description": "rank", "type": "integer" }, + "role_id": { + "description": "role id", + "type": "integer" + }, + "role_name": { + "description": "role name", + "type": "string" + }, "status": { "description": "user status(normal,suspended,deleted,inactive)", "type": "string" @@ -6061,6 +6711,14 @@ const docTemplate = `{ } } }, + "schema.ReopenQuestionReq": { + "type": "object", + "properties": { + "question_id": { + "type": "string" + } + } + }, "schema.ReportHandleReq": { "type": "object", "required": [ @@ -6221,6 +6879,48 @@ const docTemplate = `{ } } }, + "schema.SiteCustomCssHTMLReq": { + "type": "object", + "properties": { + "custom_css": { + "type": "string", + "maxLength": 65536 + }, + "custom_footer": { + "type": "string", + "maxLength": 65536 + }, + "custom_head": { + "type": "string", + "maxLength": 65536 + }, + "custom_header": { + "type": "string", + "maxLength": 65536 + } + } + }, + "schema.SiteCustomCssHTMLResp": { + "type": "object", + "properties": { + "custom_css": { + "type": "string", + "maxLength": 65536 + }, + "custom_footer": { + "type": "string", + "maxLength": 65536 + }, + "custom_head": { + "type": "string", + "maxLength": 65536 + }, + "custom_header": { + "type": "string", + "maxLength": 65536 + } + } + }, "schema.SiteGeneralReq": { "type": "object", "required": [ @@ -6281,6 +6981,32 @@ const docTemplate = `{ } } }, + "schema.SiteInfoResp": { + "type": "object", + "properties": { + "branding": { + "$ref": "#/definitions/schema.SiteBrandingResp" + }, + "custom_css_html": { + "$ref": "#/definitions/schema.SiteCustomCssHTMLResp" + }, + "general": { + "$ref": "#/definitions/schema.SiteGeneralResp" + }, + "interface": { + "$ref": "#/definitions/schema.SiteInterfaceResp" + }, + "login": { + "$ref": "#/definitions/schema.SiteLoginResp" + }, + "site__seo": { + "$ref": "#/definitions/schema.SiteSeoReq" + }, + "theme": { + "$ref": "#/definitions/schema.SiteThemeResp" + } + } + }, "schema.SiteInterfaceReq": { "type": "object", "required": [ @@ -6359,6 +7085,96 @@ const docTemplate = `{ } } }, + "schema.SiteLoginReq": { + "type": "object", + "properties": { + "allow_new_registrations": { + "type": "boolean" + }, + "login_required": { + "type": "boolean" + } + } + }, + "schema.SiteLoginResp": { + "type": "object", + "properties": { + "allow_new_registrations": { + "type": "boolean" + }, + "login_required": { + "type": "boolean" + } + } + }, + "schema.SiteSeoReq": { + "type": "object", + "required": [ + "permalink", + "robots" + ], + "properties": { + "permalink": { + "type": "integer", + "maximum": 3, + "minimum": 0 + }, + "robots": { + "type": "string" + } + } + }, + "schema.SiteSeoResp": { + "type": "object", + "required": [ + "permalink", + "robots" + ], + "properties": { + "permalink": { + "type": "integer", + "maximum": 3, + "minimum": 0 + }, + "robots": { + "type": "string" + } + } + }, + "schema.SiteThemeReq": { + "type": "object", + "required": [ + "theme" + ], + "properties": { + "theme": { + "type": "string", + "maxLength": 255 + }, + "theme_config": { + "type": "object", + "additionalProperties": true + } + } + }, + "schema.SiteThemeResp": { + "type": "object", + "properties": { + "theme": { + "type": "string" + }, + "theme_config": { + "type": "object", + "additionalProperties": true + }, + "theme_options": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.ThemeOption" + } + } + } + }, "schema.SiteWriteReq": { "type": "object", "properties": { @@ -6464,6 +7280,17 @@ const docTemplate = `{ } } }, + "schema.ThemeOption": { + "type": "object", + "properties": { + "label": { + "type": "string" + }, + "value": { + "type": "string" + } + } + }, "schema.UnreviewedRevisionInfoInfo": { "type": "object", "properties": { @@ -6671,6 +7498,40 @@ const docTemplate = `{ } } }, + "schema.UpdateUserPasswordReq": { + "type": "object", + "required": [ + "password", + "user_id" + ], + "properties": { + "password": { + "type": "string", + "maxLength": 32, + "minLength": 8 + }, + "user_id": { + "type": "string" + } + } + }, + "schema.UpdateUserRoleReq": { + "type": "object", + "required": [ + "role_id", + "user_id" + ], + "properties": { + "role_id": { + "description": "role id", + "type": "integer" + }, + "user_id": { + "description": "user id", + "type": "string" + } + } + }, "schema.UpdateUserStatusReq": { "type": "object", "required": [ @@ -6705,6 +7566,10 @@ const docTemplate = `{ "description": "display_name", "type": "string" }, + "id": { + "description": "user_id", + "type": "string" + }, "ip_info": { "description": "ip info", "type": "string" @@ -6820,6 +7685,54 @@ const docTemplate = `{ } } }, + "schema.UserRankingResp": { + "type": "object", + "properties": { + "staffs": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.UserRankingSimpleInfo" + } + }, + "users_with_the_most_reputation": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.UserRankingSimpleInfo" + } + }, + "users_with_the_most_vote": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.UserRankingSimpleInfo" + } + } + } + }, + "schema.UserRankingSimpleInfo": { + "type": "object", + "properties": { + "avatar": { + "description": "avatar", + "type": "string" + }, + "display_name": { + "description": "display name", + "type": "string" + }, + "rank": { + "description": "rank", + "type": "integer" + }, + "username": { + "description": "username", + "type": "string" + }, + "vote_count": { + "description": "vote", + "type": "integer" + } + } + }, "schema.UserRePassWordRequest": { "type": "object", "required": [ @@ -6847,6 +7760,14 @@ const docTemplate = `{ "pass" ], "properties": { + "captcha_code": { + "description": "captcha_code", + "type": "string" + }, + "captcha_id": { + "description": "captcha_id", + "type": "string" + }, "e_mail": { "description": "email", "type": "string", diff --git a/docs/swagger.json b/docs/swagger.json index 05fdf0af..913c2834 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -93,7 +93,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/entity.AdminSetAnswerStatusRequest" + "$ref": "#/definitions/schema.AdminSetAnswerStatusRequest" } } ], @@ -420,6 +420,41 @@ } } }, + "/answer/admin/api/roles": { + "get": { + "description": "get role list", + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "get role list", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.GetRoleResp" + } + } + } + } + ] + } + } + } + } + }, "/answer/admin/api/setting/smtp": { "get": { "security": [ @@ -562,6 +597,77 @@ } } }, + "/answer/admin/api/siteinfo/custom-css-html": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "get site info custom html css config", + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "get site info custom html css config", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/schema.SiteCustomCssHTMLResp" + } + } + } + ] + } + } + } + }, + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "update site custom css html config", + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "update site custom css html config", + "parameters": [ + { + "description": "login info", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.SiteCustomCssHTMLReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, "/answer/admin/api/siteinfo/general": { "get": { "security": [ @@ -775,6 +881,219 @@ } } }, + "/answer/admin/api/siteinfo/login": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "get site info login config", + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "get site info login config", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/schema.SiteLoginResp" + } + } + } + ] + } + } + } + }, + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "update site login", + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "update site login", + "parameters": [ + { + "description": "login info", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.SiteLoginReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, + "/answer/admin/api/siteinfo/seo": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "get site seo information", + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "get site seo information", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/schema.SiteSeoResp" + } + } + } + ] + } + } + } + }, + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "update site seo information", + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "update site seo information", + "parameters": [ + { + "description": "seo", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.SiteSeoReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, + "/answer/admin/api/siteinfo/theme": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "get site info theme config", + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "get site info theme config", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/schema.SiteThemeResp" + } + } + } + ] + } + } + } + }, + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "update site custom css html config", + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "update site custom css html config", + "parameters": [ + { + "description": "login info", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.SiteThemeReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, "/answer/admin/api/siteinfo/write": { "get": { "security": [ @@ -871,6 +1190,123 @@ } } }, + "/answer/admin/api/user": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "add user", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "add user", + "parameters": [ + { + "description": "user", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.AddUserReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, + "/answer/admin/api/user/password": { + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "update user password", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "update user password", + "parameters": [ + { + "description": "user", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.UpdateUserPasswordReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, + "/answer/admin/api/user/role": { + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "update user role", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "update user role", + "parameters": [ + { + "description": "user", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.UpdateUserRoleReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, "/answer/admin/api/user/status": { "put": { "security": [ @@ -944,6 +1380,12 @@ "name": "query", "in": "query" }, + { + "type": "boolean", + "description": "staff user", + "name": "staff", + "in": "query" + }, { "enum": [ "suspended", @@ -2654,6 +3096,45 @@ } } }, + "/answer/api/v1/question/reopen": { + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "reopen question", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "api-question" + ], + "summary": "reopen question", + "parameters": [ + { + "description": "question", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.ReopenQuestionReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, "/answer/api/v1/question/search": { "post": { "description": "SearchQuestionList", @@ -3254,7 +3735,7 @@ "type": "object", "properties": { "data": { - "$ref": "#/definitions/schema.SiteGeneralResp" + "$ref": "#/definitions/schema.SiteInfoResp" } } } @@ -4184,6 +4665,81 @@ } } }, + "/answer/api/v1/user/ranking": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "get user ranking", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "get user ranking", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/schema.UserRankingResp" + } + } + } + ] + } + } + } + } + }, + "/answer/api/v1/user/register/captcha": { + "get": { + "description": "UserRegisterCaptcha", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "UserRegisterCaptcha", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/schema.GetUserResp" + } + } + } + ] + } + } + } + } + }, "/answer/api/v1/user/register/email": { "post": { "description": "UserRegisterByEmail", @@ -4332,6 +4888,26 @@ } } }, + "/custom.css": { + "get": { + "description": "get site robots information", + "produces": [ + "application/json" + ], + "tags": [ + "site" + ], + "summary": "get site robots information", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "string" + } + } + } + } + }, "/installation/base-info": { "post": { "description": "init base info", @@ -4580,20 +5156,29 @@ } } } + }, + "/robots.txt": { + "get": { + "description": "get site robots information", + "produces": [ + "application/json" + ], + "tags": [ + "site" + ], + "summary": "get site robots information", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "string" + } + } + } + } } }, "definitions": { - "entity.AdminSetAnswerStatusRequest": { - "type": "object", - "properties": { - "answer_id": { - "type": "string" - }, - "status": { - "type": "string" - } - } - }, "handler.RespBody": { "type": "object", "properties": { @@ -4720,6 +5305,9 @@ "display_name": { "type": "string" }, + "main_tag_slug_name": { + "type": "string" + }, "object_type": { "type": "string" }, @@ -4755,6 +5343,9 @@ "created_at": { "type": "integer" }, + "id": { + "type": "string" + }, "object_id": { "type": "string" }, @@ -4842,6 +5433,40 @@ } } }, + "schema.AddUserReq": { + "type": "object", + "required": [ + "display_name", + "email", + "password" + ], + "properties": { + "display_name": { + "type": "string", + "maxLength": 30 + }, + "email": { + "type": "string", + "maxLength": 500 + }, + "password": { + "type": "string", + "maxLength": 32, + "minLength": 8 + } + } + }, + "schema.AdminSetAnswerStatusRequest": { + "type": "object", + "properties": { + "answer_id": { + "type": "string" + }, + "status": { + "type": "string" + } + } + }, "schema.AdminSetQuestionStatusRequest": { "type": "object", "properties": { @@ -4877,7 +5502,6 @@ "type": "string" }, "question_id": { - "description": "question_id", "type": "string" } } @@ -5374,6 +5998,20 @@ } } }, + "schema.GetRoleResp": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + } + } + }, "schema.GetSMTPConfigResp": { "type": "object", "properties": { @@ -5483,6 +6121,10 @@ "description": "created time", "type": "integer" }, + "description": { + "description": "description text", + "type": "string" + }, "display_name": { "description": "display name", "type": "string" @@ -5602,6 +6244,14 @@ "description": "rank", "type": "integer" }, + "role_id": { + "description": "role id", + "type": "integer" + }, + "role_name": { + "description": "role name", + "type": "string" + }, "status": { "description": "user status(normal,suspended,deleted,inactive)", "type": "string" @@ -6049,6 +6699,14 @@ } } }, + "schema.ReopenQuestionReq": { + "type": "object", + "properties": { + "question_id": { + "type": "string" + } + } + }, "schema.ReportHandleReq": { "type": "object", "required": [ @@ -6209,6 +6867,48 @@ } } }, + "schema.SiteCustomCssHTMLReq": { + "type": "object", + "properties": { + "custom_css": { + "type": "string", + "maxLength": 65536 + }, + "custom_footer": { + "type": "string", + "maxLength": 65536 + }, + "custom_head": { + "type": "string", + "maxLength": 65536 + }, + "custom_header": { + "type": "string", + "maxLength": 65536 + } + } + }, + "schema.SiteCustomCssHTMLResp": { + "type": "object", + "properties": { + "custom_css": { + "type": "string", + "maxLength": 65536 + }, + "custom_footer": { + "type": "string", + "maxLength": 65536 + }, + "custom_head": { + "type": "string", + "maxLength": 65536 + }, + "custom_header": { + "type": "string", + "maxLength": 65536 + } + } + }, "schema.SiteGeneralReq": { "type": "object", "required": [ @@ -6269,6 +6969,32 @@ } } }, + "schema.SiteInfoResp": { + "type": "object", + "properties": { + "branding": { + "$ref": "#/definitions/schema.SiteBrandingResp" + }, + "custom_css_html": { + "$ref": "#/definitions/schema.SiteCustomCssHTMLResp" + }, + "general": { + "$ref": "#/definitions/schema.SiteGeneralResp" + }, + "interface": { + "$ref": "#/definitions/schema.SiteInterfaceResp" + }, + "login": { + "$ref": "#/definitions/schema.SiteLoginResp" + }, + "site__seo": { + "$ref": "#/definitions/schema.SiteSeoReq" + }, + "theme": { + "$ref": "#/definitions/schema.SiteThemeResp" + } + } + }, "schema.SiteInterfaceReq": { "type": "object", "required": [ @@ -6347,6 +7073,96 @@ } } }, + "schema.SiteLoginReq": { + "type": "object", + "properties": { + "allow_new_registrations": { + "type": "boolean" + }, + "login_required": { + "type": "boolean" + } + } + }, + "schema.SiteLoginResp": { + "type": "object", + "properties": { + "allow_new_registrations": { + "type": "boolean" + }, + "login_required": { + "type": "boolean" + } + } + }, + "schema.SiteSeoReq": { + "type": "object", + "required": [ + "permalink", + "robots" + ], + "properties": { + "permalink": { + "type": "integer", + "maximum": 3, + "minimum": 0 + }, + "robots": { + "type": "string" + } + } + }, + "schema.SiteSeoResp": { + "type": "object", + "required": [ + "permalink", + "robots" + ], + "properties": { + "permalink": { + "type": "integer", + "maximum": 3, + "minimum": 0 + }, + "robots": { + "type": "string" + } + } + }, + "schema.SiteThemeReq": { + "type": "object", + "required": [ + "theme" + ], + "properties": { + "theme": { + "type": "string", + "maxLength": 255 + }, + "theme_config": { + "type": "object", + "additionalProperties": true + } + } + }, + "schema.SiteThemeResp": { + "type": "object", + "properties": { + "theme": { + "type": "string" + }, + "theme_config": { + "type": "object", + "additionalProperties": true + }, + "theme_options": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.ThemeOption" + } + } + } + }, "schema.SiteWriteReq": { "type": "object", "properties": { @@ -6452,6 +7268,17 @@ } } }, + "schema.ThemeOption": { + "type": "object", + "properties": { + "label": { + "type": "string" + }, + "value": { + "type": "string" + } + } + }, "schema.UnreviewedRevisionInfoInfo": { "type": "object", "properties": { @@ -6659,6 +7486,40 @@ } } }, + "schema.UpdateUserPasswordReq": { + "type": "object", + "required": [ + "password", + "user_id" + ], + "properties": { + "password": { + "type": "string", + "maxLength": 32, + "minLength": 8 + }, + "user_id": { + "type": "string" + } + } + }, + "schema.UpdateUserRoleReq": { + "type": "object", + "required": [ + "role_id", + "user_id" + ], + "properties": { + "role_id": { + "description": "role id", + "type": "integer" + }, + "user_id": { + "description": "user id", + "type": "string" + } + } + }, "schema.UpdateUserStatusReq": { "type": "object", "required": [ @@ -6693,6 +7554,10 @@ "description": "display_name", "type": "string" }, + "id": { + "description": "user_id", + "type": "string" + }, "ip_info": { "description": "ip info", "type": "string" @@ -6808,6 +7673,54 @@ } } }, + "schema.UserRankingResp": { + "type": "object", + "properties": { + "staffs": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.UserRankingSimpleInfo" + } + }, + "users_with_the_most_reputation": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.UserRankingSimpleInfo" + } + }, + "users_with_the_most_vote": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.UserRankingSimpleInfo" + } + } + } + }, + "schema.UserRankingSimpleInfo": { + "type": "object", + "properties": { + "avatar": { + "description": "avatar", + "type": "string" + }, + "display_name": { + "description": "display name", + "type": "string" + }, + "rank": { + "description": "rank", + "type": "integer" + }, + "username": { + "description": "username", + "type": "string" + }, + "vote_count": { + "description": "vote", + "type": "integer" + } + } + }, "schema.UserRePassWordRequest": { "type": "object", "required": [ @@ -6835,6 +7748,14 @@ "pass" ], "properties": { + "captcha_code": { + "description": "captcha_code", + "type": "string" + }, + "captcha_id": { + "description": "captcha_id", + "type": "string" + }, "e_mail": { "description": "email", "type": "string", diff --git a/docs/swagger.yaml b/docs/swagger.yaml index ae9e1bf8..06837519 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1,11 +1,4 @@ definitions: - entity.AdminSetAnswerStatusRequest: - properties: - answer_id: - type: string - status: - type: string - type: object handler.RespBody: properties: code: @@ -95,6 +88,8 @@ definitions: type: string display_name: type: string + main_tag_slug_name: + type: string object_type: type: string question_id: @@ -118,6 +113,8 @@ definitions: type: string created_at: type: integer + id: + type: string object_id: type: string object_type: @@ -179,6 +176,30 @@ definitions: - object_id - report_type type: object + schema.AddUserReq: + properties: + display_name: + maxLength: 30 + type: string + email: + maxLength: 500 + type: string + password: + maxLength: 32 + minLength: 8 + type: string + required: + - display_name + - email + - password + type: object + schema.AdminSetAnswerStatusRequest: + properties: + answer_id: + type: string + status: + type: string + type: object schema.AdminSetQuestionStatusRequest: properties: question_id: @@ -203,7 +224,6 @@ definitions: answer_id: type: string question_id: - description: question_id type: string type: object schema.AnswerUpdateReq: @@ -562,6 +582,15 @@ definitions: user_info: $ref: '#/definitions/schema.UserBasicInfo' type: object + schema.GetRoleResp: + properties: + description: + type: string + id: + type: integer + name: + type: string + type: object schema.GetSMTPConfigResp: properties: encryption: @@ -638,6 +667,9 @@ definitions: created_at: description: created time type: integer + description: + description: description text + type: string display_name: description: display name type: string @@ -724,6 +756,12 @@ definitions: rank: description: rank type: integer + role_id: + description: role id + type: integer + role_name: + description: role name + type: string status: description: user status(normal,suspended,deleted,inactive) type: string @@ -1052,6 +1090,11 @@ definitions: required: - tag_id type: object + schema.ReopenQuestionReq: + properties: + question_id: + type: string + type: object schema.ReportHandleReq: properties: flagged_content: @@ -1163,6 +1206,36 @@ definitions: - logo - square_icon type: object + schema.SiteCustomCssHTMLReq: + properties: + custom_css: + maxLength: 65536 + type: string + custom_footer: + maxLength: 65536 + type: string + custom_head: + maxLength: 65536 + type: string + custom_header: + maxLength: 65536 + type: string + type: object + schema.SiteCustomCssHTMLResp: + properties: + custom_css: + maxLength: 65536 + type: string + custom_footer: + maxLength: 65536 + type: string + custom_head: + maxLength: 65536 + type: string + custom_header: + maxLength: 65536 + type: string + type: object schema.SiteGeneralReq: properties: contact_email: @@ -1207,6 +1280,23 @@ definitions: - name - site_url type: object + schema.SiteInfoResp: + properties: + branding: + $ref: '#/definitions/schema.SiteBrandingResp' + custom_css_html: + $ref: '#/definitions/schema.SiteCustomCssHTMLResp' + general: + $ref: '#/definitions/schema.SiteGeneralResp' + interface: + $ref: '#/definitions/schema.SiteInterfaceResp' + login: + $ref: '#/definitions/schema.SiteLoginResp' + site__seo: + $ref: '#/definitions/schema.SiteSeoReq' + theme: + $ref: '#/definitions/schema.SiteThemeResp' + type: object schema.SiteInterfaceReq: properties: language: @@ -1261,6 +1351,67 @@ definitions: terms_of_service_parsed_text: type: string type: object + schema.SiteLoginReq: + properties: + allow_new_registrations: + type: boolean + login_required: + type: boolean + type: object + schema.SiteLoginResp: + properties: + allow_new_registrations: + type: boolean + login_required: + type: boolean + type: object + schema.SiteSeoReq: + properties: + permalink: + maximum: 3 + minimum: 0 + type: integer + robots: + type: string + required: + - permalink + - robots + type: object + schema.SiteSeoResp: + properties: + permalink: + maximum: 3 + minimum: 0 + type: integer + robots: + type: string + required: + - permalink + - robots + type: object + schema.SiteThemeReq: + properties: + theme: + maxLength: 255 + type: string + theme_config: + additionalProperties: true + type: object + required: + - theme + type: object + schema.SiteThemeResp: + properties: + theme: + type: string + theme_config: + additionalProperties: true + type: object + theme_options: + items: + $ref: '#/definitions/schema.ThemeOption' + type: array + type: object schema.SiteWriteReq: properties: recommend_tags: @@ -1335,6 +1486,13 @@ definitions: description: tag id type: string type: object + schema.ThemeOption: + properties: + label: + type: string + value: + type: string + type: object schema.UnreviewedRevisionInfoInfo: properties: content: @@ -1483,6 +1641,30 @@ definitions: required: - language type: object + schema.UpdateUserPasswordReq: + properties: + password: + maxLength: 32 + minLength: 8 + type: string + user_id: + type: string + required: + - password + - user_id + type: object + schema.UpdateUserRoleReq: + properties: + role_id: + description: role id + type: integer + user_id: + description: user id + type: string + required: + - role_id + - user_id + type: object schema.UpdateUserStatusReq: properties: status: @@ -1508,6 +1690,9 @@ definitions: display_name: description: display_name type: string + id: + description: user_id + type: string ip_info: description: ip info type: string @@ -1589,6 +1774,39 @@ definitions: notice_switch: type: boolean type: object + schema.UserRankingResp: + properties: + staffs: + items: + $ref: '#/definitions/schema.UserRankingSimpleInfo' + type: array + users_with_the_most_reputation: + items: + $ref: '#/definitions/schema.UserRankingSimpleInfo' + type: array + users_with_the_most_vote: + items: + $ref: '#/definitions/schema.UserRankingSimpleInfo' + type: array + type: object + schema.UserRankingSimpleInfo: + properties: + avatar: + description: avatar + type: string + display_name: + description: display name + type: string + rank: + description: rank + type: integer + username: + description: username + type: string + vote_count: + description: vote + type: integer + type: object schema.UserRePassWordRequest: properties: code: @@ -1605,6 +1823,12 @@ definitions: type: object schema.UserRegisterReq: properties: + captcha_code: + description: captcha_code + type: string + captcha_id: + description: captcha_id + type: string e_mail: description: email maxLength: 500 @@ -1722,7 +1946,7 @@ paths: name: data required: true schema: - $ref: '#/definitions/entity.AdminSetAnswerStatusRequest' + $ref: '#/definitions/schema.AdminSetAnswerStatusRequest' produces: - application/json responses: @@ -1935,6 +2159,26 @@ paths: summary: list report page tags: - admin + /answer/admin/api/roles: + get: + description: get role list + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/handler.RespBody' + - properties: + data: + items: + $ref: '#/definitions/schema.GetRoleResp' + type: array + type: object + summary: get role list + tags: + - admin /answer/admin/api/setting/smtp: get: description: GetSMTPConfig get smtp config @@ -2017,6 +2261,47 @@ paths: summary: update site info branding tags: - admin + /answer/admin/api/siteinfo/custom-css-html: + get: + description: get site info custom html css config + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/handler.RespBody' + - properties: + data: + $ref: '#/definitions/schema.SiteCustomCssHTMLResp' + type: object + security: + - ApiKeyAuth: [] + summary: get site info custom html css config + tags: + - admin + put: + description: update site custom css html config + parameters: + - description: login info + in: body + name: data + required: true + schema: + $ref: '#/definitions/schema.SiteCustomCssHTMLReq' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handler.RespBody' + security: + - ApiKeyAuth: [] + summary: update site custom css html config + tags: + - admin /answer/admin/api/siteinfo/general: get: description: get site general information @@ -2140,6 +2425,129 @@ paths: summary: update site legal info tags: - admin + /answer/admin/api/siteinfo/login: + get: + description: get site info login config + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/handler.RespBody' + - properties: + data: + $ref: '#/definitions/schema.SiteLoginResp' + type: object + security: + - ApiKeyAuth: [] + summary: get site info login config + tags: + - admin + put: + description: update site login + parameters: + - description: login info + in: body + name: data + required: true + schema: + $ref: '#/definitions/schema.SiteLoginReq' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handler.RespBody' + security: + - ApiKeyAuth: [] + summary: update site login + tags: + - admin + /answer/admin/api/siteinfo/seo: + get: + description: get site seo information + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/handler.RespBody' + - properties: + data: + $ref: '#/definitions/schema.SiteSeoResp' + type: object + security: + - ApiKeyAuth: [] + summary: get site seo information + tags: + - admin + put: + description: update site seo information + parameters: + - description: seo + in: body + name: data + required: true + schema: + $ref: '#/definitions/schema.SiteSeoReq' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handler.RespBody' + security: + - ApiKeyAuth: [] + summary: update site seo information + tags: + - admin + /answer/admin/api/siteinfo/theme: + get: + description: get site info theme config + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/handler.RespBody' + - properties: + data: + $ref: '#/definitions/schema.SiteThemeResp' + type: object + security: + - ApiKeyAuth: [] + summary: get site info theme config + tags: + - admin + put: + description: update site custom css html config + parameters: + - description: login info + in: body + name: data + required: true + schema: + $ref: '#/definitions/schema.SiteThemeReq' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handler.RespBody' + security: + - ApiKeyAuth: [] + summary: update site custom css html config + tags: + - admin /answer/admin/api/siteinfo/write: get: description: get site interface @@ -2196,6 +2604,78 @@ paths: summary: Get theme options tags: - admin + /answer/admin/api/user: + post: + consumes: + - application/json + description: add user + parameters: + - description: user + in: body + name: data + required: true + schema: + $ref: '#/definitions/schema.AddUserReq' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handler.RespBody' + security: + - ApiKeyAuth: [] + summary: add user + tags: + - admin + /answer/admin/api/user/password: + put: + consumes: + - application/json + description: update user password + parameters: + - description: user + in: body + name: data + required: true + schema: + $ref: '#/definitions/schema.UpdateUserPasswordReq' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handler.RespBody' + security: + - ApiKeyAuth: [] + summary: update user password + tags: + - admin + /answer/admin/api/user/role: + put: + consumes: + - application/json + description: update user role + parameters: + - description: user + in: body + name: data + required: true + schema: + $ref: '#/definitions/schema.UpdateUserRoleReq' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handler.RespBody' + security: + - ApiKeyAuth: [] + summary: update user role + tags: + - admin /answer/admin/api/user/status: put: consumes: @@ -2236,6 +2716,10 @@ paths: in: query name: query type: string + - description: staff user + in: query + name: staff + type: boolean - description: user status enum: - suspended @@ -3279,6 +3763,30 @@ paths: summary: SearchQuestionList tags: - api-question + /answer/api/v1/question/reopen: + put: + consumes: + - application/json + description: reopen question + parameters: + - description: question + in: body + name: data + required: true + schema: + $ref: '#/definitions/schema.ReopenQuestionReq' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handler.RespBody' + security: + - ApiKeyAuth: [] + summary: reopen question + tags: + - api-question /answer/api/v1/question/search: post: consumes: @@ -3646,7 +4154,7 @@ paths: - $ref: '#/definitions/handler.RespBody' - properties: data: - $ref: '#/definitions/schema.SiteGeneralResp' + $ref: '#/definitions/schema.SiteInfoResp' type: object summary: get site info tags: @@ -4213,6 +4721,48 @@ paths: summary: RetrievePassWord tags: - User + /answer/api/v1/user/ranking: + get: + consumes: + - application/json + description: get user ranking + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/handler.RespBody' + - properties: + data: + $ref: '#/definitions/schema.UserRankingResp' + type: object + security: + - ApiKeyAuth: [] + summary: get user ranking + tags: + - User + /answer/api/v1/user/register/captcha: + get: + consumes: + - application/json + description: UserRegisterCaptcha + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/handler.RespBody' + - properties: + data: + $ref: '#/definitions/schema.GetUserResp' + type: object + summary: UserRegisterCaptcha + tags: + - User /answer/api/v1/user/register/email: post: consumes: @@ -4298,6 +4848,19 @@ paths: summary: vote up tags: - Activity + /custom.css: + get: + description: get site robots information + produces: + - application/json + responses: + "200": + description: OK + schema: + type: string + summary: get site robots information + tags: + - site /installation/base-info: post: consumes: @@ -4453,6 +5016,19 @@ paths: summary: UserList tags: - api-question + /robots.txt: + get: + description: get site robots information + produces: + - application/json + responses: + "200": + description: OK + schema: + type: string + summary: get site robots information + tags: + - site securityDefinitions: ApiKeyAuth: in: header diff --git a/go.mod b/go.mod index 13bd9f47..78fd6031 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/Chain-Zhang/pinyin v0.1.3 github.com/anargu/gin-brotli v0.0.0-20220116052358-12bf532d5267 github.com/bwmarrin/snowflake v0.3.0 + github.com/davecgh/go-spew v1.1.1 github.com/disintegration/imaging v1.6.2 github.com/gin-gonic/gin v1.8.1 github.com/go-playground/locales v0.14.0 @@ -13,8 +14,11 @@ require ( github.com/go-playground/validator/v10 v10.11.1 github.com/go-sql-driver/mysql v1.6.0 github.com/goccy/go-json v0.9.11 + github.com/golang/mock v1.4.4 + github.com/gomarkdown/markdown v0.0.0-20221013030248-663e2500819c github.com/google/uuid v1.3.0 github.com/google/wire v0.5.0 + github.com/gosimple/slug v1.13.1 github.com/grokify/html-strip-tags-go v0.0.1 github.com/jinzhu/copier v0.3.5 github.com/jinzhu/now v1.1.5 @@ -22,10 +26,11 @@ require ( github.com/mattn/go-sqlite3 v1.14.16 github.com/mojocn/base64Captcha v1.3.5 github.com/ory/dockertest/v3 v3.9.1 + github.com/robfig/cron/v3 v3.0.1 github.com/segmentfault/pacman v1.0.1 github.com/segmentfault/pacman/contrib/cache/memory v0.0.0-20221018072427-a15dd1434e05 github.com/segmentfault/pacman/contrib/conf/viper v0.0.0-20221018072427-a15dd1434e05 - github.com/segmentfault/pacman/contrib/i18n v0.0.0-20221109042453-26158da67632 + github.com/segmentfault/pacman/contrib/i18n v0.0.0-20221207032920-3662d1e32068 github.com/segmentfault/pacman/contrib/log/zap v0.0.0-20221018072427-a15dd1434e05 github.com/segmentfault/pacman/contrib/server/http v0.0.0-20221018072427-a15dd1434e05 github.com/spf13/cobra v1.6.1 @@ -45,12 +50,12 @@ require ( require ( github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 // indirect github.com/KyleBanks/depth v1.2.1 // indirect + github.com/LinkinStars/go-i18n/v2 v2.2.2 // indirect github.com/Microsoft/go-winio v0.5.2 // indirect github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect github.com/andybalholm/brotli v1.0.4 // indirect github.com/cenkalti/backoff/v4 v4.1.3 // indirect github.com/containerd/continuity v0.3.0 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect github.com/docker/cli v20.10.14+incompatible // indirect github.com/docker/docker v20.10.7+incompatible // indirect github.com/docker/go-connections v0.4.0 // indirect @@ -65,7 +70,7 @@ require ( github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect github.com/golang/snappy v0.0.4 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect - github.com/google/subcommands v1.0.1 // indirect + github.com/gosimple/unidecode v1.0.1 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/imdario/mergo v0.3.12 // indirect github.com/inconshreveable/mousetrap v1.0.1 // indirect @@ -81,7 +86,6 @@ require ( github.com/moby/term v0.0.0-20201216013528-df9cb8a40635 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/nicksnyder/go-i18n/v2 v2.2.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.0.2 // indirect github.com/opencontainers/runc v1.1.2 // indirect @@ -106,9 +110,8 @@ require ( go.uber.org/multierr v1.8.0 // indirect go.uber.org/zap v1.23.0 // indirect golang.org/x/image v0.1.0 // indirect - golang.org/x/mod v0.6.0 // indirect golang.org/x/sys v0.1.0 // indirect - golang.org/x/text v0.4.0 // indirect + golang.org/x/text v0.5.0 // indirect golang.org/x/tools v0.2.0 // indirect google.golang.org/protobuf v1.28.1 // indirect gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect diff --git a/go.sum b/go.sum index 8bfba639..dec4922a 100644 --- a/go.sum +++ b/go.sum @@ -50,6 +50,8 @@ github.com/Chain-Zhang/pinyin v0.1.3/go.mod h1:5iHpt9p4znrnaP59/hfPMnAojajkDxQaP github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= +github.com/LinkinStars/go-i18n/v2 v2.2.2 h1:ZfjpzbW13dv6btv3RALKZkpN9A+7K1JA//2QcNeWaxU= +github.com/LinkinStars/go-i18n/v2 v2.2.2/go.mod h1:hLglSJ4/3M0Y7ZVcoEJI+OwqkglHCA32DdjuJJR2LbM= github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= github.com/Microsoft/go-winio v0.5.2 h1:a9IhgEQBCUEk6QCdml9CiJGhAws+YwffDHEMp1VMrpA= github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= @@ -233,6 +235,7 @@ github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFU github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc= github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= 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= @@ -253,6 +256,8 @@ github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8l github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/gomarkdown/markdown v0.0.0-20221013030248-663e2500819c h1:iyaGYbCmcYK0Ja9a3OUa2Fo+EaN0cbLu0eKpBwPFzc8= +github.com/gomarkdown/markdown v0.0.0-20221013030248-663e2500819c/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= @@ -284,7 +289,6 @@ github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLe github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= -github.com/google/subcommands v1.0.1 h1:/eqq+otEXm5vhfBrbREPCSVQbvofip6kIz+mX5TUH7k= github.com/google/subcommands v1.0.1/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -300,6 +304,10 @@ github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51 github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gosimple/slug v1.13.1 h1:bQ+kpX9Qa6tHRaK+fZR0A0M2Kd7Pa5eHPPsb1JpHD+Q= +github.com/gosimple/slug v1.13.1/go.mod h1:UiRaFH+GEilHstLUmcBgWcI42viBN7mAb818JrYOeFQ= +github.com/gosimple/unidecode v1.0.1 h1:hZzFTMMqSswvf0LBJZCZgThIZrpDHFXux9KeGmn6T/o= +github.com/gosimple/unidecode v1.0.1/go.mod h1:CP0Cr1Y1kogOtx0bJblKzsVWrqYaqfNOnHzpgWw4Awc= github.com/grokify/html-strip-tags-go v0.0.1 h1:0fThFwLbW7P/kOiTBs03FsJSV9RM2M/Q/MOnCQxKMo0= github.com/grokify/html-strip-tags-go v0.0.1/go.mod h1:2Su6romC5/1VXOQMaWL2yb618ARB8iVo6/DR99A6d78= github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= @@ -499,8 +507,6 @@ github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzE github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= -github.com/nicksnyder/go-i18n/v2 v2.2.0 h1:MNXbyPvd141JJqlU6gJKrczThxJy+kdCNivxZpBQFkw= -github.com/nicksnyder/go-i18n/v2 v2.2.0/go.mod h1:4OtLfzqyAxsscyCb//3gfqSvBc81gImX91LrZzczN1o= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs= github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= @@ -576,6 +582,8 @@ github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+Gx github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk= github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +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.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= @@ -597,8 +605,8 @@ github.com/segmentfault/pacman/contrib/cache/memory v0.0.0-20221018072427-a15dd1 github.com/segmentfault/pacman/contrib/cache/memory v0.0.0-20221018072427-a15dd1434e05/go.mod h1:rmf1TCwz67dyM+AmTwSd1BxTo2AOYHj262lP93bOZbs= github.com/segmentfault/pacman/contrib/conf/viper v0.0.0-20221018072427-a15dd1434e05 h1:BlqTgc3/MYKG6vMI2MI+6o+7P4Gy5PXlawu185wPXAk= github.com/segmentfault/pacman/contrib/conf/viper v0.0.0-20221018072427-a15dd1434e05/go.mod h1:prPjFam7MyZ5b3S9dcDOt2tMPz6kf7C9c243s9zSwPY= -github.com/segmentfault/pacman/contrib/i18n v0.0.0-20221109042453-26158da67632 h1:so07u8RWXZQ0gz30KXJ9MKtQ5zjgcDlQ/UwFZrwm5b0= -github.com/segmentfault/pacman/contrib/i18n v0.0.0-20221109042453-26158da67632/go.mod h1:5Afm+OQdau/HQqSOp/ALlSUp0vZsMMMbv//kJhxuoi8= +github.com/segmentfault/pacman/contrib/i18n v0.0.0-20221207032920-3662d1e32068 h1:ln/qgrC62e7/XHGPiikWFV4dyYgCaWeZYkmSGqrHZp4= +github.com/segmentfault/pacman/contrib/i18n v0.0.0-20221207032920-3662d1e32068/go.mod h1:7QcRmnV7OYq4hNOOCWXT5HXnN/u756JUsqIW0Bw8n9E= github.com/segmentfault/pacman/contrib/log/zap v0.0.0-20221018072427-a15dd1434e05 h1:jcGZU2juv0L3eFEkuZYV14ESLUlWfGMWnP0mjOfrSZc= github.com/segmentfault/pacman/contrib/log/zap v0.0.0-20221018072427-a15dd1434e05/go.mod h1:L4GqtXLoR73obTYqUQIzfkm8NG8pvZafxFb6KZFSSHk= github.com/segmentfault/pacman/contrib/server/http v0.0.0-20221018072427-a15dd1434e05 h1:91is1nKNbfTOl8CvMYiFgg4c5Vmol+5mVmMV/jDXD+A= @@ -780,7 +788,6 @@ golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.6.0 h1:b9gGHsz9/HhJ3HF5DHQytPpuwocVTChQJK3AvoLRD5I= -golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI= 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= @@ -930,8 +937,9 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/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.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM= +golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= diff --git a/i18n/en_US.yaml b/i18n/en_US.yaml index 555bb439..e5a67c36 100644 --- a/i18n/en_US.yaml +++ b/i18n/en_US.yaml @@ -13,6 +13,22 @@ backend: database_error: other: "Data server error." + role: + name: + user: + other: "User" + admin: + other: "Admin" + moderator: + other: "Moderator" + description: + user: + other: "Default with no special access." + admin: + other: "Have the full power to access the site." + moderator: + other: "Has access to all posts except admin settings." + email: other: "Email" password: @@ -93,6 +109,8 @@ backend: other: "Should not contain synonym tags." cannot_update: other: "No permission to update." + cannot_set_synonym_as_itself: + other: "You cannot set the synonym of the current tag as itself." theme: not_found: other: "Theme not found." @@ -125,36 +143,40 @@ backend: install: create_config_failed: other: "Can’t create the config.yaml file." + cannot_update_your_role: + other: "You cannot modify your role." + not_allowed_registration: + other: "Currently the site is not open for registration" report: spam: name: other: "spam" - description: + desc: other: "This post is an advertisement, or vandalism. It is not useful or relevant to the current topic." rude: name: other: "rude or abusive" - description: + desc: other: "A reasonable person would find this content inappropriate for respectful discourse." duplicate: name: other: "a duplicate" - description: + desc: other: "This question has been asked before and already has an answer." not_answer: name: other: "not an answer" - description: + desc: other: "This was posted as an answer, but it does not attempt to answer the question. It should possibly be an edit, a comment, another question, or deleted altogether." not_need: name: other: "no longer needed" - description: + desc: other: "This comment is outdated, conversational or not relevant to this post." other: name: other: "something else" - description: + desc: other: "This post requires staff attention for another reason not listed above." question: @@ -162,22 +184,22 @@ backend: duplicate: name: other: "spam" - description: + desc: other: "This question has been asked before and already has an answer." guideline: name: other: "a community-specific reason" - description: + desc: other: "This question doesn't meet a community guideline." multiple: name: other: "needs details or clarity" - description: + desc: other: "This question currently includes multiple questions in one. It should focus on one problem only." other: name: other: "something else" - description: + desc: other: "This post requires another reason not listed above." notification: @@ -206,11 +228,12 @@ backend: other: "Your answer has been deleted" your_comment_was_deleted: other: "Your comment has been deleted" + # The following fields are used for interface presentation(Front-end) ui: how_to_format: title: How to Format - description: >- + desc: >-
to make links
<https://url.com>
[Title](https://url.com)put returns between paragraphs
-
- Built on <1> Answer 1>- the open-source software that power Q&A
+ Built on <1> Answer 1>- the open-source software that powers Q&A
communities.
Made with love © {{cc}}.
upload_img:
name: Change
@@ -640,7 +664,8 @@ ui:
account: Account
interface: Interface
profile:
- btn_name: Update profile
+ heading: Profile
+ btn_name: Save
display_name:
label: Display Name
msg: Display name cannot be empty.
@@ -658,7 +683,7 @@ ui:
custom: Custom
btn_refresh: Refresh
custom_text: You can upload your image.
- default: Default
+ default: System
msg: Please upload an avatar
bio:
label: About Me (optional)
@@ -670,10 +695,12 @@ ui:
label: Location (optional)
placeholder: "City, Country"
notification:
+ heading: Notifications
email:
label: Email Notifications
radio: "Answers to your questions, comments, and more"
account:
+ heading: Account
change_email_btn: Change email
change_pass_btn: Change password
change_email_info: >-
@@ -694,6 +721,7 @@ ui:
pass_confirm:
label: Confirm New Password
interface:
+ heading: Interface
lang:
label: Interface Language
text: User interface language. It will change when you refresh the page.
@@ -701,7 +729,7 @@ ui:
update: update success
update_password: Password changed successfully.
flag_success: Thanks for flagging.
- fobidden_operate_self: Forbidden to operate on yourself
+ forbidden_operate_self: Forbidden to operate on yourself
review: Your revision will show after review.
related_question:
title: Related Questions
@@ -727,12 +755,17 @@ ui:
write_answer:
title: Your Answer
btn_name: Post your answer
+ add_another_answer: Add another answer
confirm_title: Continue to answer
continue: Continue
confirm_info: >-
Are you sure you want to add another answer?
You could use the edit link to refine and improve your existing answer, instead.
empty: Answer cannot be empty. + reopen: + title: Reopen this post + content: Are you sure you want to reopen? + success: This post has been reopened delete: title: Delete this post question: >- @@ -893,7 +926,7 @@ ui: config_yaml: title: Create config.yaml label: The config.yaml file created. - description: >- + desc: >- You can create the <1>config.yaml1> file manually in the <1>/var/wwww/xxx/1> directory and paste the following text into it. info: "After you’ve done that, click “Next” button." @@ -930,31 +963,31 @@ ui: empty: Email cannot be empty. incorrect: Email incorrect format. ready_title: Your Answer is Ready! - ready_description: >- + ready_desc: >- If you ever feel like changing more settings, visit <1>admin section1>; find it in the site menu. good_luck: "Have fun, and good luck!" warn_title: Warning - warn_description: >- + warn_desc: >- The file <1>config.yaml1> already exists. If you need to reset any of the configuration items in this file, please delete it first. install_now: You may try <1>installing now1>. installed: Already installed - installed_description: >- + installed_desc: >- You appear to have already installed. To reinstall please clear your old database tables first. db_failed: Database connection failed - db_failed_description: >- + db_failed_desc: >- This either means that the database information in your <1>config.yaml1> file is incorrect or that contact with the database server could not be established. This could mean your host’s database server is down. page_404: - description: "Unfortunately, this page doesn't exist." + desc: "Unfortunately, this page doesn't exist." back_home: Back to homepage page_50X: - description: The server encountered an error and could not complete your request. + desc: The server encountered an error and could not complete your request. back_home: Back to homepage page_maintenance: - description: "We are under maintenance, we’ll be back soon." + desc: "We are under maintenance, we’ll be back soon." nav_menus: dashboard: Dashboard contents: Contents @@ -971,6 +1004,11 @@ ui: write: Write tos: Terms of Service privacy: Privacy + seo: SEO + customize: Customize + themes: Themes + css-html: CSS/HTML + login: Login admin: admin_header: title: Admin @@ -1020,13 +1058,13 @@ ui: btn_cancel: Cancel btn_submit: Submit normal_name: normal - normal_description: A normal user can ask and answer questions. + normal_desc: A normal user can ask and answer questions. suspended_name: suspended - suspended_description: A suspended user can't log in. + suspended_desc: A suspended user can't log in. deleted_name: deleted - deleted_description: "Delete profile, authentication associations." + deleted_desc: "Delete profile, authentication associations." inactive_name: inactive - inactive_description: An inactive user must re-validate their email. + inactive_desc: An inactive user must re-validate their email. confirm_title: Delete this user confirm_content: Are you sure you want to delete this user? This is permanent! confirm_btn: Delete @@ -1035,11 +1073,11 @@ ui: status_modal: title: "Change {{ type }} status to..." normal_name: normal - normal_description: A normal post available to everyone. + normal_desc: A normal post available to everyone. closed_name: closed - closed_description: "A closed question can't answer, but still can edit, vote and comment." + closed_desc: "A closed question can't answer, but still can edit, vote and comment." deleted_name: deleted - deleted_description: All reputation gained and lost will be restored. + deleted_desc: All reputation gained and lost will be restored. btn_cancel: Cancel btn_submit: Submit btn_next: Next @@ -1074,6 +1112,34 @@ ui: change_status: Change status change_role: Change role show_logs: Show logs + add_user: Add user + new_password_modal: + title: Set new password + form: + fields: + password: + label: Password + text: The user will be logged out and need to login again. + msg: Password must be at 8 - 32 characters in length. + btn_cancel: Cancel + btn_submit: Submit + user_modal: + title: Add new user + form: + fields: + display_name: + label: Display Name + msg: display_name must be at 4 - 30 characters in length. + email: + label: Email + msg: Email is not valid. + password: + label: Password + msg: Password must be at 8 - 32 characters in length. + + btn_cancel: Cancel + btn_submit: Submit + questions: page_title: Questions normal: Normal @@ -1111,11 +1177,11 @@ ui: msg: Site url cannot be empty. validate: Please enter a valid URL. text: The address of your site. - short_description: + short_desc: label: Short Site Description (optional) msg: Short site description cannot be empty. text: "Short description, as used in the title tag on homepage." - description: + desc: label: Site Description (optional) msg: Site description cannot be empty. text: "Describe this site in one sentence, as used in the meta description tag." @@ -1177,7 +1243,8 @@ ui: text: Provide email address that will receive test sends. msg: Test email recipients is invalid smtp_authentication: - label: SMTP Authentication + label: Enable authentication + title: SMTP Authentication msg: SMTP authentication cannot be empty. "yes": "Yes" "no": "No" @@ -1217,6 +1284,50 @@ ui: reserved_tags: label: Reserved Tags text: "Reserved tags can only be added to a post by moderator." + seo: + page_title: SEO + permalink: + label: Permalink + text: Custom URL structures can improve the usability, and forward-compatibility of your links. + robots: + label: robots.txt + text: This will permanently override any related site settings. + themes: + page_title: Themes + themes: + label: Themes + text: Select an existing theme. + navbar_style: + label: Navbar Style + text: Select an existing theme. + primary_color: + label: Primary Color + text: Modify the colors used by your themes + css_and_html: + page_title: CSS and HTML + custom_css: + label: Custom CSS + text: This will insert as + head: + label: Head + text: This will insert before + header: + label: Header + text: This will insert after + footer: + label: Footer + text: This will insert before . + login: + page_title: Login + membership: + title: Membership + label: Allow new registrations + text: Turn off to prevent anyone from creating a new account. + private: + title: Private + label: Login required + text: Only logged in users can access this community. + form: empty: cannot be empty invalid: is invalid @@ -1260,3 +1371,12 @@ ui: by: By comment: Comment no_data: "We couldn't find anything." + users: + title: Users + users_with_the_most_reputation: Users with the highest reputation scores + users_with_the_most_vote: Users who voted the most + staffs: Our community staff + reputation: reputation + votes: votes + + diff --git a/i18n/zh_CN.yaml b/i18n/zh_CN.yaml index 413dc19f..7a50f1d4 100644 --- a/i18n/zh_CN.yaml +++ b/i18n/zh_CN.yaml @@ -11,6 +11,22 @@ backend: database_error: other: "数据服务异常" + role: + name: + user: + other: "用户" + admin: + other: "管理员" + moderator: + other: "版主" + description: + user: + other: "默认没有特殊访问权限。" + admin: + other: "拥有进入网站的全部权限。" + moderator: + other: "有权访问所有的帖子,无法进入管理员设置页面。" + email: other: "邮箱" password: @@ -81,6 +97,8 @@ backend: other: "不应包含同义词标签。" cannot_update: other: "没有更新标签权限。" + cannot_set_synonym_as_itself: + other: "你无法将当前标签的同义词设置为当前标签自己" theme: not_found: other: "主题未找到" @@ -97,6 +115,10 @@ backend: other: "用户名已被使用" set_avatar: other: "头像设置错误" + cannot_update_your_role: + other: "你无法修改自己的角色" + not_allowed_registration: + other: "目前该网站尚未开放注册" revision: review_underway: other: "目前无法编辑,有一个版本在审阅队列中。" @@ -188,7 +210,7 @@ backend: ui: how_to_format: title: 如何设定文本格式 - description: >- + desc: >-添加链接:
<https://url.com>
[标题](https://url.com)段落之间使用空行分隔
_斜体_ 或者
@@ -293,7 +315,7 @@ ui:
empty: 请选择图片文件。
only_image: 只能上传图片文件。
max_size: 图片文件大小不能超过 4 MB。
- description:
+ desc:
label: 图片描述(可选)
tab_url: 网络图片
form_url:
@@ -365,12 +387,12 @@ ui:
range: 不能超过 35 个字符
slug_name:
label: URL 固定链接
- description: '必须由 "a-z", "0-9", "+ # - ." 组成'
+ desc: '必须由 "a-z", "0-9", "+ # - ." 组成'
msg:
empty: 不能为空
range: 不能超过 35 个字符
character: 包含非法字符
- description:
+ desc:
label: 标签描述(可选)
btn_cancel: 取消
btn_submit: 提交
@@ -402,7 +424,7 @@ ui:
slug_name:
label: URL 固定链接
info: '必须由 "a-z", "0-9", "+ # - ." 组成'
- description:
+ desc:
label: 描述
edit_summary:
label: 编辑概要
@@ -453,7 +475,7 @@ ui:
button_following: 已关注
tag_label: 个问题
search_placeholder: 通过标签名过滤
- no_description: 此标签无描述。
+ no_desc: 此标签无描述。
more: 更多
ask:
title: 提交新的问题
@@ -505,7 +527,7 @@ ui:
placeholder: 搜索
footer:
build_on: >-
- Built on <1> Answer 1>- the open-source software that power Q&A
+ Built on <1> Answer 1>- the open-source software that powers Q&A
communities
Made with love © 2022 Answer
upload_img:
name: 更改图片
@@ -767,10 +789,10 @@ ui:
x_answers: 个回答
x_questions: 个问题
page_404:
- description: 页面不存在
+ desc: 页面不存在
back_home: 回到主页
page_50X:
- description: 服务器遇到了一个错误,无法完成你的请求。
+ desc: 服务器遇到了一个错误,无法完成你的请求。
back_home: 回到主页
nav_menus:
dashboard: 后台管理
@@ -803,13 +825,13 @@ ui:
btn_cancel: 取消
btn_submit: 提交
normal_name: 正常
- normal_description: 正常状态的用户可以提问和回答。
+ normal_desc: 正常状态的用户可以提问和回答。
suspended_name: 封禁
- suspended_description: 被封禁的用户将无法登录。
+ suspended_desc: 被封禁的用户将无法登录。
deleted_name: 删除
- deleted_description: 删除用户的个人信息,认证等等。
+ deleted_desc: 删除用户的个人信息,认证等等。
inactive_name: 不活跃
- inactive_description: 不活跃的用户必须重新验证邮箱。
+ inactive_desc: 不活跃的用户必须重新验证邮箱。
confirm_title: 删除此用户
confirm_content: 确定要删除此用户?此操作无法撤销!
confirm_btn: 删除
@@ -818,11 +840,11 @@ ui:
status_modal:
title: '更改 {{ type }} 状态为...'
normal_name: 正常
- normal_description: 所有用户都可以访问
+ normal_desc: 所有用户都可以访问
closed_name: 关闭
- closed_description: 不能回答,但仍然可以编辑、投票和评论。
+ closed_desc: 不能回答,但仍然可以编辑、投票和评论。
deleted_name: 删除
- deleted_description: 所有获得/损失的声望将会恢复。
+ deleted_desc: 所有获得/损失的声望将会恢复。
btn_cancel: 取消
btn_submit: 提交
btn_next: 下一步
@@ -870,11 +892,11 @@ ui:
label: 站点名称
msg: 不能为空
text: 站点的名称,作为站点的标题(HTML 的 title 标签)。
- short_description:
+ short_desc:
label: 简短的站点标语 (可选)
msg: 不能为空
text: 简短的标语,作为网站主页的标题(HTML 的 title 标签)。
- description:
+ desc:
label: 网站描述 (可选)
msg: 不能为空
text: 使用一句话描述本站,作为网站的描述(HTML 的 meta 标签)。
diff --git a/internal/base/conf/conf.go b/internal/base/conf/conf.go
index 18a841d9..615405f1 100644
--- a/internal/base/conf/conf.go
+++ b/internal/base/conf/conf.go
@@ -4,6 +4,8 @@ import (
"bytes"
"path/filepath"
+ "github.com/answerdev/answer/configs"
+ "github.com/answerdev/answer/internal/base/constant"
"github.com/answerdev/answer/internal/base/data"
"github.com/answerdev/answer/internal/base/server"
"github.com/answerdev/answer/internal/base/translator"
@@ -12,6 +14,7 @@ import (
"github.com/answerdev/answer/internal/service/service_config"
"github.com/answerdev/answer/pkg/writer"
"github.com/segmentfault/pacman/contrib/conf/viper"
+ "github.com/segmentfault/pacman/log"
"gopkg.in/yaml.v3"
)
@@ -25,6 +28,10 @@ type AllConfig struct {
Swaggerui *router.SwaggerConfig `json:"swaggerui" mapstructure:"swaggerui" yaml:"swaggerui"`
}
+type PathIgnore struct {
+ Users []string `yaml:"users"`
+}
+
// Server server config
type Server struct {
HTTP *server.HTTP `json:"http" mapstructure:"http" yaml:"http"`
@@ -62,3 +69,18 @@ func RewriteConfig(configFilePath string, allConfig *AllConfig) error {
}
return writer.ReplaceFile(configFilePath, buf.String())
}
+
+func GetPathIgnoreList() map[string]bool {
+ list := make(map[string]bool, 0)
+ data := &PathIgnore{}
+ err := yaml.Unmarshal(configs.PathIgnore, data)
+ if err != nil {
+ log.Error(err)
+ return list
+ }
+ for _, item := range data.Users {
+ list[item] = true
+ }
+ constant.PathIgnoreMap = list
+ return list
+}
diff --git a/internal/base/constant/constant.go b/internal/base/constant/constant.go
index c36a0f7f..40990b93 100644
--- a/internal/base/constant/constant.go
+++ b/internal/base/constant/constant.go
@@ -11,6 +11,9 @@ const (
AdminTokenCacheKey = "answer:admin:token:"
AdminTokenCacheTime = 7 * 24 * time.Hour
AcceptLanguageFlag = "Accept-Language"
+ UserTokenMappingCacheKey = "answer:user-token:mapping:"
+ SiteInfoCacheKey = "answer:site-info:"
+ SiteInfoCacheTime = 1 * time.Hour
)
const (
@@ -29,6 +32,8 @@ const (
var (
Version string = ""
+ PathIgnoreMap map[string]bool
+
ObjectTypeStrMapping = map[string]int{
QuestionObjectType: 1,
AnswerObjectType: 2,
@@ -51,10 +56,18 @@ var (
)
const (
- SiteTypeGeneral = "general"
- SiteTypeInterface = "interface"
- SiteTypeBranding = "branding"
- SiteTypeWrite = "write"
- SiteTypeLegal = "legal"
+ SiteTypeGeneral = "general"
+ SiteTypeInterface = "interface"
+ SiteTypeBranding = "branding"
+ SiteTypeWrite = "write"
+ SiteTypeLegal = "legal"
+ SiteTypeSeo = "seo"
+ SiteTypeLogin = "login"
+ SiteTypeCustomCssHTML = "css-html"
+ SiteTypeTheme = "theme"
)
+func ExistInPathIgnore(name string) bool {
+ _, ok := PathIgnoreMap[name]
+ return ok
+}
diff --git a/internal/base/cron/cron.go b/internal/base/cron/cron.go
new file mode 100644
index 00000000..79c34597
--- /dev/null
+++ b/internal/base/cron/cron.go
@@ -0,0 +1,44 @@
+package cron
+
+import (
+ "context"
+ "fmt"
+
+ "github.com/answerdev/answer/internal/service"
+ "github.com/answerdev/answer/internal/service/siteinfo_common"
+ "github.com/robfig/cron/v3"
+ "github.com/segmentfault/pacman/log"
+)
+
+// ScheduledTaskManager scheduled task manager
+type ScheduledTaskManager struct {
+ siteInfoService *siteinfo_common.SiteInfoCommonService
+ questionService *service.QuestionService
+}
+
+// NewScheduledTaskManager new scheduled task manager
+func NewScheduledTaskManager(
+ siteInfoService *siteinfo_common.SiteInfoCommonService,
+ questionService *service.QuestionService,
+) *ScheduledTaskManager {
+ manager := &ScheduledTaskManager{
+ siteInfoService: siteInfoService,
+ questionService: questionService,
+ }
+ return manager
+}
+
+func (s *ScheduledTaskManager) Run() {
+ fmt.Println("start cron")
+ s.questionService.SitemapCron(context.Background())
+ c := cron.New()
+ _, err := c.AddFunc("0 */1 * * *", func() {
+ ctx := context.Background()
+ fmt.Println("sitemap cron execution")
+ s.questionService.SitemapCron(ctx)
+ })
+ if err != nil {
+ log.Error(err)
+ }
+ c.Start()
+}
diff --git a/internal/base/cron/provider.go b/internal/base/cron/provider.go
new file mode 100644
index 00000000..5dc7f93b
--- /dev/null
+++ b/internal/base/cron/provider.go
@@ -0,0 +1,10 @@
+package cron
+
+import (
+ "github.com/google/wire"
+)
+
+// ProviderSetService is providers.
+var ProviderSetService = wire.NewSet(
+ NewScheduledTaskManager,
+)
diff --git a/internal/base/handler/handler.go b/internal/base/handler/handler.go
index 3b4a5b65..8e7ef118 100644
--- a/internal/base/handler/handler.go
+++ b/internal/base/handler/handler.go
@@ -59,3 +59,17 @@ func BindAndCheck(ctx *gin.Context, data interface{}) bool {
}
return false
}
+
+// BindAndCheckReturnErr bind request and check
+func BindAndCheckReturnErr(ctx *gin.Context, data interface{}) (errFields []*validator.FormErrorField) {
+ lang := GetLang(ctx)
+ if err := ctx.ShouldBind(data); err != nil {
+ log.Errorf("http_handle BindAndCheck fail, %s", err.Error())
+ HandleResponse(ctx, myErrors.New(http.StatusBadRequest, reason.RequestFormatError), nil)
+ ctx.Abort()
+ return nil
+ }
+
+ errFields, _ = validator.GetValidatorByLang(lang.Abbr()).Check(data)
+ return errFields
+}
diff --git a/internal/base/middleware/accept_language.go b/internal/base/middleware/accept_language.go
new file mode 100644
index 00000000..01cdcdab
--- /dev/null
+++ b/internal/base/middleware/accept_language.go
@@ -0,0 +1,13 @@
+package middleware
+
+import (
+ "github.com/answerdev/answer/internal/base/constant"
+ "github.com/answerdev/answer/internal/base/handler"
+ "github.com/gin-gonic/gin"
+)
+
+// ExtractAndSetAcceptLanguage extract accept language from header and set to context
+func ExtractAndSetAcceptLanguage(ctx *gin.Context) {
+ lang := handler.GetLang(ctx)
+ ctx.Set(constant.AcceptLanguageFlag, lang)
+}
diff --git a/internal/base/middleware/auth.go b/internal/base/middleware/auth.go
index e793d3c7..555a2d10 100644
--- a/internal/base/middleware/auth.go
+++ b/internal/base/middleware/auth.go
@@ -4,6 +4,7 @@ import (
"strings"
"github.com/answerdev/answer/internal/schema"
+ "github.com/answerdev/answer/internal/service/siteinfo_common"
"github.com/answerdev/answer/internal/base/handler"
"github.com/answerdev/answer/internal/base/reason"
@@ -18,13 +19,17 @@ var ctxUUIDKey = "ctxUuidKey"
// AuthUserMiddleware auth user middleware
type AuthUserMiddleware struct {
- authService *auth.AuthService
+ authService *auth.AuthService
+ siteInfoCommonService *siteinfo_common.SiteInfoCommonService
}
// NewAuthUserMiddleware new auth user middleware
-func NewAuthUserMiddleware(authService *auth.AuthService) *AuthUserMiddleware {
+func NewAuthUserMiddleware(
+ authService *auth.AuthService,
+ siteInfoCommonService *siteinfo_common.SiteInfoCommonService) *AuthUserMiddleware {
return &AuthUserMiddleware{
- authService: authService,
+ authService: authService,
+ siteInfoCommonService: siteInfoCommonService,
}
}
@@ -48,6 +53,29 @@ func (am *AuthUserMiddleware) Auth() gin.HandlerFunc {
}
}
+// EjectUserBySiteInfo if admin config the site can access by nologin user, eject user.
+func (am *AuthUserMiddleware) EjectUserBySiteInfo() gin.HandlerFunc {
+ return func(ctx *gin.Context) {
+ mustLogin := false
+ siteInfo, _ := am.siteInfoCommonService.GetSiteLogin(ctx)
+ if siteInfo != nil {
+ mustLogin = siteInfo.LoginRequired
+ }
+ if !mustLogin {
+ ctx.Next()
+ return
+ }
+
+ _, isLogin := ctx.Get(ctxUUIDKey)
+ if !isLogin {
+ handler.HandleResponse(ctx, errors.Unauthorized(reason.UnauthorizedError), nil)
+ ctx.Abort()
+ return
+ }
+ ctx.Next()
+ }
+}
+
// MustAuth auth user info. If the user does not log in, an unauthenticated error is displayed
func (am *AuthUserMiddleware) MustAuth() gin.HandlerFunc {
return func(ctx *gin.Context) {
diff --git a/internal/base/middleware/avatar.go b/internal/base/middleware/avatar.go
index 3a6ce2da..66a72f68 100644
--- a/internal/base/middleware/avatar.go
+++ b/internal/base/middleware/avatar.go
@@ -2,8 +2,8 @@ package middleware
import (
"fmt"
- "io/ioutil"
"net/url"
+ "os"
"path/filepath"
"strings"
@@ -11,6 +11,7 @@ import (
"github.com/answerdev/answer/internal/service/uploader"
"github.com/answerdev/answer/pkg/converter"
"github.com/gin-gonic/gin"
+ "github.com/segmentfault/pacman/log"
)
type AvatarMiddleware struct {
@@ -44,7 +45,7 @@ func (am *AvatarMiddleware) AvatarThumb() gin.HandlerFunc {
filePath := fmt.Sprintf("%s/avatar/%s", uploadPath, urlfileName)
var avatarfile []byte
if size == 0 {
- avatarfile, err = ioutil.ReadFile(filePath)
+ avatarfile, err = os.ReadFile(filePath)
} else {
avatarfile, err = am.uploaderService.AvatarThumbFile(ctx, uploadPath, urlfileName, size)
}
@@ -52,7 +53,10 @@ func (am *AvatarMiddleware) AvatarThumb() gin.HandlerFunc {
ctx.Next()
return
}
- ctx.Writer.WriteString(string(avatarfile))
+ _, err = ctx.Writer.WriteString(string(avatarfile))
+ if err != nil {
+ log.Error(err)
+ }
ctx.Abort()
return
diff --git a/internal/base/reason/reason.go b/internal/base/reason/reason.go
index 21dbf341..915098e0 100644
--- a/internal/base/reason/reason.go
+++ b/internal/base/reason/reason.go
@@ -57,4 +57,7 @@ const (
RecommendTagEnter = "error.tag.recommend_tag_enter"
RevisionReviewUnderway = "error.revision.review_underway"
RevisionNoPermission = "error.revision.no_permission"
+ UserCannotUpdateYourRole = "error.user.cannot_update_your_role"
+ TagCannotSetSynonymAsItself = "error.tag.cannot_set_synonym_as_itself"
+ NotAllowedRegistration = "error.user.not_allowed_registration"
)
diff --git a/internal/base/server/http.go b/internal/base/server/http.go
index 71220162..35341fd3 100644
--- a/internal/base/server/http.go
+++ b/internal/base/server/http.go
@@ -1,9 +1,13 @@
package server
import (
+ "html/template"
+ "io/fs"
+
brotli "github.com/anargu/gin-brotli"
"github.com/answerdev/answer/internal/base/middleware"
"github.com/answerdev/answer/internal/router"
+ "github.com/answerdev/answer/ui"
"github.com/gin-gonic/gin"
)
@@ -15,6 +19,7 @@ func NewHTTPServer(debug bool,
viewRouter *router.UIRouter,
authUserMiddleware *middleware.AuthUserMiddleware,
avatarMiddleware *middleware.AvatarMiddleware,
+ templateRouter *router.TemplateRouter,
) *gin.Engine {
if debug {
@@ -23,9 +28,13 @@ func NewHTTPServer(debug bool,
gin.SetMode(gin.ReleaseMode)
}
r := gin.New()
- r.Use(brotli.Brotli(brotli.DefaultCompression))
+ r.Use(brotli.Brotli(brotli.DefaultCompression), middleware.ExtractAndSetAcceptLanguage)
r.GET("/healthz", func(ctx *gin.Context) { ctx.String(200, "OK") })
+ html, _ := fs.Sub(ui.Template, "template")
+ htmlTemplate := template.Must(template.New("").Funcs(funcMap).ParseFS(html, "*"))
+ r.SetHTMLTemplate(htmlTemplate)
+
viewRouter.Register(r)
rootGroup := r.Group("")
@@ -34,9 +43,13 @@ func NewHTTPServer(debug bool,
static.Use(avatarMiddleware.AvatarThumb())
staticRouter.RegisterStaticRouter(static)
+ // The route must be available without logging in
+ mustUnAuthV1 := r.Group("/answer/api/v1")
+ answerRouter.RegisterMustUnAuthAnswerAPIRouter(mustUnAuthV1)
+
// register api that no need to login
unAuthV1 := r.Group("/answer/api/v1")
- unAuthV1.Use(authUserMiddleware.Auth())
+ unAuthV1.Use(authUserMiddleware.Auth(), authUserMiddleware.EjectUserBySiteInfo())
answerRouter.RegisterUnAuthAnswerAPIRouter(unAuthV1)
// register api that must be authenticated
@@ -48,5 +61,6 @@ func NewHTTPServer(debug bool,
cmsauthV1.Use(authUserMiddleware.CmsAuth())
answerRouter.RegisterAnswerCmsAPIRouter(cmsauthV1)
+ templateRouter.RegisterTemplateRouter(rootGroup)
return r
}
diff --git a/internal/base/server/http_funcmap.go b/internal/base/server/http_funcmap.go
new file mode 100644
index 00000000..caac20f4
--- /dev/null
+++ b/internal/base/server/http_funcmap.go
@@ -0,0 +1,117 @@
+package server
+
+import (
+ "html/template"
+ "math"
+ "regexp"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/answerdev/answer/internal/base/translator"
+ "github.com/answerdev/answer/internal/schema"
+ "github.com/answerdev/answer/pkg/converter"
+ "github.com/answerdev/answer/pkg/day"
+ "github.com/answerdev/answer/pkg/htmltext"
+ "github.com/segmentfault/pacman/i18n"
+)
+
+var funcMap = template.FuncMap{
+ "replaceHTMLTag": func(src string, tags ...string) string {
+ p := `(?U)<(\d+)>.+(\d+)>`
+
+ re := regexp.MustCompile(p)
+ ms := re.FindAllStringSubmatch(src, -1)
+ for _, mi := range ms {
+ if mi[1] == mi[2] {
+ i, err := strconv.Atoi(mi[1])
+ if err != nil || len(tags) < i {
+ break
+ }
+
+ src = strings.ReplaceAll(src, mi[0], tags[i-1])
+ }
+ }
+
+ return src
+ },
+ "join": func(sep string, elems ...string) string {
+ return strings.Join(elems, sep)
+ },
+ "templateHTML": func(data string) template.HTML {
+ return template.HTML(data)
+ },
+ "translator": func(la i18n.Language, data string, params ...interface{}) string {
+ trans := translator.GlobalTrans.Tr(la, data)
+
+ if len(params) > 0 && len(params)%2 == 0 {
+ for i := 0; i < len(params); i += 2 {
+ k := converter.InterfaceToString(params[i])
+ v := converter.InterfaceToString(params[i+1])
+ trans = strings.ReplaceAll(trans, "{{ "+k+" }}", v)
+ }
+ }
+
+ return trans
+ },
+ "timeFormatISO": func(tz string, timestamp int64) string {
+ _, _ = time.LoadLocation(tz)
+ return time.Unix(timestamp, 0).Format("2006-01-02T15:04:05.000Z")
+ },
+ "translatorTimeFormatLongDate": func(la i18n.Language, tz string, timestamp int64) string {
+ trans := translator.GlobalTrans.Tr(la, "ui.dates.long_date_with_time")
+ return day.Format(timestamp, trans, tz)
+ },
+ "translatorTimeFormat": func(la i18n.Language, tz string, timestamp int64) string {
+ var (
+ now = time.Now().Unix()
+ between int64 = 0
+ trans string
+ )
+ _, _ = time.LoadLocation(tz)
+ if now > timestamp {
+ between = now - timestamp
+ }
+
+ if between <= 1 {
+ return translator.GlobalTrans.Tr(la, "ui.dates.now")
+ }
+
+ if between > 1 && between < 60 {
+ trans = translator.GlobalTrans.Tr(la, "ui.dates.x_seconds_ago")
+ return strings.ReplaceAll(trans, "{{count}}", converter.IntToString(between))
+ }
+
+ if between >= 60 && between < 3600 {
+ min := math.Floor(float64(between / 60))
+ trans = translator.GlobalTrans.Tr(la, "ui.dates.x_minutes_ago")
+ return strings.ReplaceAll(trans, "{{count}}", strconv.FormatFloat(min, 'f', 0, 64))
+ }
+
+ if between >= 3600 && between < 3600*24 {
+ h := math.Floor(float64(between / 3600))
+ trans = translator.GlobalTrans.Tr(la, "ui.dates.x_hours_ago")
+ return strings.ReplaceAll(trans, "{{count}}", strconv.FormatFloat(h, 'f', 0, 64))
+ }
+
+ if between >= 3600*24 &&
+ between < 3600*24*366 &&
+ time.Unix(timestamp, 0).Format("2006") == time.Unix(now, 0).Format("2006") {
+ trans = translator.GlobalTrans.Tr(la, "ui.dates.long_date")
+ return day.Format(timestamp, trans, tz)
+ }
+
+ trans = translator.GlobalTrans.Tr(la, "ui.dates.long_date_with_year")
+ return day.Format(timestamp, trans, tz)
+ },
+ "wrapComments": func(comments []*schema.GetCommentResp, la i18n.Language, tz string) map[string]interface{} {
+ return map[string]interface{}{
+ "comments": comments,
+ "language": la,
+ "timezone": tz,
+ }
+ },
+ "urlTitle": func(title string) string {
+ return htmltext.UrlTitle(title)
+ },
+}
diff --git a/internal/base/translator/provider.go b/internal/base/translator/provider.go
index 527f9137..518132ea 100644
--- a/internal/base/translator/provider.go
+++ b/internal/base/translator/provider.go
@@ -51,14 +51,22 @@ func NewTranslator(c *I18n) (tr i18n.Translator, err error) {
return nil, fmt.Errorf("read file failed: %s %s", file.Name(), err)
}
- // only parse the backend translation
- translation := struct {
- Content map[string]interface{} `yaml:"backend"`
+ // parse the backend translation
+ originalTr := struct {
+ Backend map[string]map[string]interface{} `yaml:"backend"`
+ UI map[string]interface{} `yaml:"ui"`
}{}
- if err = yaml.Unmarshal(buf, &translation); err != nil {
+ if err = yaml.Unmarshal(buf, &originalTr); err != nil {
return nil, err
}
- content, err := yaml.Marshal(translation.Content)
+ translation := make(map[string]interface{}, 0)
+ for k, v := range originalTr.Backend {
+ translation[k] = v
+ }
+ translation["backend"] = originalTr.Backend
+ translation["ui"] = originalTr.UI
+
+ content, err := yaml.Marshal(translation)
if err != nil {
return nil, fmt.Errorf("marshal translation content failed: %s %s", file.Name(), err)
}
diff --git a/internal/base/validator/validator.go b/internal/base/validator/validator.go
index 554d079c..9c9b8017 100644
--- a/internal/base/validator/validator.go
+++ b/internal/base/validator/validator.go
@@ -124,9 +124,13 @@ func (m *MyValidator) Check(value interface{}) (errFields []*FormErrorField, err
if v, ok := value.(Checker); ok {
errFields, err = v.Check()
- if err != nil {
- return errFields, err
+ if err == nil {
+ return nil, nil
}
+ for _, errField := range errFields {
+ errField.ErrorMsg = translator.GlobalTrans.Tr(m.Lang, errField.ErrorMsg)
+ }
+ return errFields, err
}
return nil, nil
}
diff --git a/internal/controller/answer_controller.go b/internal/controller/answer_controller.go
index 2b91d779..7ca1f898 100644
--- a/internal/controller/answer_controller.go
+++ b/internal/controller/answer_controller.go
@@ -9,6 +9,7 @@ import (
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service"
"github.com/answerdev/answer/internal/service/dashboard"
+ "github.com/answerdev/answer/internal/service/permission"
"github.com/answerdev/answer/internal/service/rank"
"github.com/gin-gonic/gin"
"github.com/segmentfault/pacman/errors"
@@ -51,7 +52,7 @@ func (ac *AnswerController) RemoveAnswer(ctx *gin.Context) {
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
req.IsAdmin = middleware.GetIsAdminFromContext(ctx)
- can, err := ac.rankService.CheckOperationPermission(ctx, req.UserID, rank.AnswerDeleteRank, req.ID)
+ can, err := ac.rankService.CheckOperationPermission(ctx, req.UserID, permission.AnswerDelete, req.ID)
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
@@ -110,7 +111,7 @@ func (ac *AnswerController) Add(ctx *gin.Context) {
}
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
- can, err := ac.rankService.CheckOperationPermission(ctx, req.UserID, rank.AnswerAddRank, "")
+ can, err := ac.rankService.CheckOperationPermission(ctx, req.UserID, permission.AnswerAdd, "")
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
@@ -159,15 +160,17 @@ func (ac *AnswerController) Update(ctx *gin.Context) {
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
canList, err := ac.rankService.CheckOperationPermissions(ctx, req.UserID, []string{
- rank.AnswerEditRank,
- rank.AnswerEditWithoutReviewRank,
- }, req.ID)
+ permission.AnswerEdit,
+ permission.AnswerEditWithoutReview,
+ })
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
}
- req.CanEdit = canList[0]
- req.NoNeedReview = canList[1]
+
+ objectOwner := ac.rankService.CheckOperationObjectOwner(ctx, req.UserID, req.ID)
+ req.CanEdit = canList[0] || objectOwner
+ req.NoNeedReview = canList[1] || objectOwner
if !req.CanEdit {
handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil)
return
@@ -208,9 +211,9 @@ func (ac *AnswerController) AnswerList(ctx *gin.Context) {
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
canList, err := ac.rankService.CheckOperationPermissions(ctx, req.UserID, []string{
- rank.AnswerEditRank,
- rank.AnswerDeleteRank,
- }, "")
+ permission.AnswerEdit,
+ permission.AnswerDelete,
+ })
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
@@ -246,7 +249,7 @@ func (ac *AnswerController) Adopted(ctx *gin.Context) {
}
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
- can, err := ac.rankService.CheckOperationPermission(ctx, req.UserID, rank.AnswerAcceptRank, req.QuestionID)
+ can, err := ac.rankService.CheckOperationPermission(ctx, req.UserID, permission.AnswerAccept, req.QuestionID)
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
@@ -267,7 +270,7 @@ func (ac *AnswerController) Adopted(ctx *gin.Context) {
// @Accept json
// @Produce json
// @Security ApiKeyAuth
-// @Param data body entity.AdminSetAnswerStatusRequest true "AdminSetAnswerStatusRequest"
+// @Param data body schema.AdminSetAnswerStatusRequest true "AdminSetAnswerStatusRequest"
// @Router /answer/admin/api/answer/status [put]
// @Success 200 {object} handler.RespBody
func (ac *AnswerController) AdminSetAnswerStatus(ctx *gin.Context) {
diff --git a/internal/controller/comment_controller.go b/internal/controller/comment_controller.go
index b7a161e1..eb025139 100644
--- a/internal/controller/comment_controller.go
+++ b/internal/controller/comment_controller.go
@@ -6,6 +6,7 @@ import (
"github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/comment"
+ "github.com/answerdev/answer/internal/service/permission"
"github.com/answerdev/answer/internal/service/rank"
"github.com/gin-gonic/gin"
"github.com/segmentfault/pacman/errors"
@@ -42,10 +43,10 @@ func (cc *CommentController) AddComment(ctx *gin.Context) {
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
canList, err := cc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{
- rank.CommentAddRank,
- rank.CommentEditRank,
- rank.CommentDeleteRank,
- }, "")
+ permission.CommentAdd,
+ permission.CommentEdit,
+ permission.CommentDelete,
+ })
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
@@ -79,7 +80,7 @@ func (cc *CommentController) RemoveComment(ctx *gin.Context) {
}
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
- can, err := cc.rankService.CheckOperationPermission(ctx, req.UserID, rank.CommentDeleteRank, req.CommentID)
+ can, err := cc.rankService.CheckOperationPermission(ctx, req.UserID, permission.CommentDelete, req.CommentID)
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
@@ -110,7 +111,7 @@ func (cc *CommentController) UpdateComment(ctx *gin.Context) {
}
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
- can, err := cc.rankService.CheckOperationPermission(ctx, req.UserID, rank.CommentEditRank, req.CommentID)
+ can, err := cc.rankService.CheckOperationPermission(ctx, req.UserID, permission.CommentEdit, req.CommentID)
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
@@ -143,9 +144,9 @@ func (cc *CommentController) GetCommentWithPage(ctx *gin.Context) {
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
canList, err := cc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{
- rank.CommentEditRank,
- rank.CommentDeleteRank,
- }, "")
+ permission.CommentEdit,
+ permission.CommentDelete,
+ })
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
@@ -195,9 +196,9 @@ func (cc *CommentController) GetComment(ctx *gin.Context) {
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
canList, err := cc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{
- rank.CommentEditRank,
- rank.CommentDeleteRank,
- }, "")
+ permission.CommentEdit,
+ permission.CommentDelete,
+ })
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
diff --git a/internal/controller/controller.go b/internal/controller/controller.go
index 5b3e34f3..fcbca143 100644
--- a/internal/controller/controller.go
+++ b/internal/controller/controller.go
@@ -23,4 +23,5 @@ var ProviderSetController = wire.NewSet(
NewDashboardController,
NewUploadController,
NewActivityController,
+ NewTemplateController,
)
diff --git a/internal/controller/notification_controller.go b/internal/controller/notification_controller.go
index 91422555..f5b88238 100644
--- a/internal/controller/notification_controller.go
+++ b/internal/controller/notification_controller.go
@@ -5,6 +5,7 @@ import (
"github.com/answerdev/answer/internal/base/middleware"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/notification"
+ "github.com/answerdev/answer/internal/service/permission"
"github.com/answerdev/answer/internal/service/rank"
"github.com/gin-gonic/gin"
)
@@ -43,10 +44,10 @@ func (nc *NotificationController) GetRedDot(ctx *gin.Context) {
req.UserID = userID
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
canList, err := nc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{
- rank.QuestionAuditRank,
- rank.AnswerAuditRank,
- rank.TagAuditRank,
- }, "")
+ permission.QuestionAudit,
+ permission.AnswerAudit,
+ permission.TagAudit,
+ })
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
@@ -76,10 +77,10 @@ func (nc *NotificationController) ClearRedDot(ctx *gin.Context) {
}
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
canList, err := nc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{
- rank.QuestionAuditRank,
- rank.AnswerAuditRank,
- rank.TagAuditRank,
- }, "")
+ permission.QuestionAudit,
+ permission.AnswerAudit,
+ permission.TagAudit,
+ })
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
diff --git a/internal/controller/question_controller.go b/internal/controller/question_controller.go
index 2fe17320..7a9199b3 100644
--- a/internal/controller/question_controller.go
+++ b/internal/controller/question_controller.go
@@ -6,9 +6,11 @@ import (
"github.com/answerdev/answer/internal/base/handler"
"github.com/answerdev/answer/internal/base/middleware"
"github.com/answerdev/answer/internal/base/reason"
+ "github.com/answerdev/answer/internal/base/validator"
"github.com/answerdev/answer/internal/entity"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service"
+ "github.com/answerdev/answer/internal/service/permission"
"github.com/answerdev/answer/internal/service/rank"
"github.com/answerdev/answer/pkg/converter"
"github.com/gin-gonic/gin"
@@ -43,7 +45,7 @@ func (qc *QuestionController) RemoveQuestion(ctx *gin.Context) {
}
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
req.IsAdmin = middleware.GetIsAdminFromContext(ctx)
- can, err := qc.rankService.CheckOperationPermission(ctx, req.UserID, rank.QuestionDeleteRank, req.ID)
+ can, err := qc.rankService.CheckOperationPermission(ctx, req.UserID, permission.QuestionDelete, req.ID)
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
@@ -73,8 +75,47 @@ func (qc *QuestionController) CloseQuestion(ctx *gin.Context) {
return
}
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
- req.IsAdmin = middleware.GetIsAdminFromContext(ctx)
- err := qc.questionService.CloseQuestion(ctx, req)
+ can, err := qc.rankService.CheckOperationPermission(ctx, req.UserID, permission.QuestionClose, "")
+ if err != nil {
+ handler.HandleResponse(ctx, err, nil)
+ return
+ }
+ if !can {
+ handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil)
+ return
+ }
+
+ err = qc.questionService.CloseQuestion(ctx, req)
+ handler.HandleResponse(ctx, err, nil)
+}
+
+// ReopenQuestion reopen question
+// @Summary reopen question
+// @Description reopen question
+// @Tags api-question
+// @Accept json
+// @Produce json
+// @Security ApiKeyAuth
+// @Param data body schema.ReopenQuestionReq true "question"
+// @Success 200 {object} handler.RespBody
+// @Router /answer/api/v1/question/reopen [put]
+func (qc *QuestionController) ReopenQuestion(ctx *gin.Context) {
+ req := &schema.ReopenQuestionReq{}
+ if handler.BindAndCheck(ctx, req) {
+ return
+ }
+ req.UserID = middleware.GetLoginUserIDFromContext(ctx)
+ can, err := qc.rankService.CheckOperationPermission(ctx, req.UserID, permission.QuestionReopen, "")
+ if err != nil {
+ handler.HandleResponse(ctx, err, nil)
+ return
+ }
+ if !can {
+ handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil)
+ return
+ }
+
+ err = qc.questionService.ReopenQuestion(ctx, req)
handler.HandleResponse(ctx, err, nil)
}
@@ -93,16 +134,21 @@ func (qc *QuestionController) GetQuestion(ctx *gin.Context) {
userID := middleware.GetLoginUserIDFromContext(ctx)
req := schema.QuestionPermission{}
canList, err := qc.rankService.CheckOperationPermissions(ctx, userID, []string{
- rank.QuestionEditRank,
- rank.QuestionDeleteRank,
- }, id)
+ permission.QuestionEdit,
+ permission.QuestionDelete,
+ permission.QuestionClose,
+ permission.QuestionReopen,
+ })
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
}
- req.CanEdit = canList[0]
+ objectOwner := qc.rankService.CheckOperationObjectOwner(ctx, userID, id)
+
+ req.CanEdit = canList[0] || objectOwner
req.CanDelete = canList[1]
- req.CanClose = middleware.GetIsAdminFromContext(ctx)
+ req.CanClose = canList[2]
+ req.CanReopen = canList[3]
info, err := qc.questionService.GetQuestionAndAddPV(ctx, id, userID, req)
if err != nil {
@@ -202,16 +248,20 @@ func (qc *QuestionController) SearchList(c *gin.Context) {
// @Router /answer/api/v1/question [post]
func (qc *QuestionController) AddQuestion(ctx *gin.Context) {
req := &schema.QuestionAdd{}
- if handler.BindAndCheck(ctx, req) {
+ errFields := handler.BindAndCheckReturnErr(ctx, req)
+ if ctx.IsAborted() {
return
}
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
canList, err := qc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{
- rank.QuestionAddRank,
- rank.QuestionEditRank,
- rank.QuestionDeleteRank,
- }, "")
+ permission.QuestionAdd,
+ permission.QuestionEdit,
+ permission.QuestionDelete,
+ permission.QuestionClose,
+ permission.QuestionReopen,
+ permission.TagUseReservedTag,
+ })
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
@@ -219,13 +269,26 @@ func (qc *QuestionController) AddQuestion(ctx *gin.Context) {
req.CanAdd = canList[0]
req.CanEdit = canList[1]
req.CanDelete = canList[2]
- req.CanClose = middleware.GetIsAdminFromContext(ctx)
+ req.CanClose = canList[3]
+ req.CanReopen = canList[4]
+ req.CanUseReservedTag = canList[5]
if !req.CanAdd {
handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil)
return
}
resp, err := qc.questionService.AddQuestion(ctx, req)
+ if err != nil {
+ errlist, ok := resp.([]*validator.FormErrorField)
+ if ok {
+ errFields = append(errFields, errlist...)
+ }
+ }
+ if len(errFields) > 0 {
+ handler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), errFields)
+ return
+ }
+
handler.HandleResponse(ctx, err, resp)
}
@@ -241,33 +304,49 @@ func (qc *QuestionController) AddQuestion(ctx *gin.Context) {
// @Router /answer/api/v1/question [put]
func (qc *QuestionController) UpdateQuestion(ctx *gin.Context) {
req := &schema.QuestionUpdate{}
- if handler.BindAndCheck(ctx, req) {
+ errFields := handler.BindAndCheckReturnErr(ctx, req)
+ if ctx.IsAborted() {
return
}
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
canList, err := qc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{
- rank.QuestionEditRank,
- rank.QuestionDeleteRank,
- rank.QuestionEditWithoutReviewRank,
- }, req.ID)
+ permission.QuestionEdit,
+ permission.QuestionDelete,
+ permission.QuestionEditWithoutReview,
+ permission.TagUseReservedTag,
+ })
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
}
- req.CanEdit = canList[0]
- req.CanDelete = canList[1]
- req.NoNeedReview = canList[2]
- req.CanClose = middleware.GetIsAdminFromContext(ctx)
- req.IsAdmin = middleware.GetIsAdminFromContext(ctx)
+ objectOwner := qc.rankService.CheckOperationObjectOwner(ctx, req.UserID, req.ID)
+ req.CanEdit = canList[0] || objectOwner
+ req.CanDelete = canList[1]
+ req.NoNeedReview = canList[2] || objectOwner
+ req.CanUseReservedTag = canList[3]
if !req.CanEdit {
handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil)
return
}
- _, err = qc.questionService.UpdateQuestion(ctx, req)
- handler.HandleResponse(ctx, err, &schema.UpdateQuestionResp{WaitForReview: !req.NoNeedReview})
+ errlist, err := qc.questionService.UpdateQuestionCheckTags(ctx, req)
+ if err != nil {
+ errFields = append(errFields, errlist...)
+ }
+
+ if len(errFields) > 0 {
+ handler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), errFields)
+ return
+ }
+
+ resp, err := qc.questionService.UpdateQuestion(ctx, req)
+ if err != nil {
+ handler.HandleResponse(ctx, err, resp)
+ return
+ }
+ handler.HandleResponse(ctx, nil, &schema.UpdateQuestionResp{WaitForReview: !req.NoNeedReview})
}
// CloseMsgList close question msg list
diff --git a/internal/controller/report_controller.go b/internal/controller/report_controller.go
index 6e7141ec..85987e84 100644
--- a/internal/controller/report_controller.go
+++ b/internal/controller/report_controller.go
@@ -5,6 +5,7 @@ import (
"github.com/answerdev/answer/internal/base/middleware"
"github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/internal/schema"
+ "github.com/answerdev/answer/internal/service/permission"
"github.com/answerdev/answer/internal/service/rank"
"github.com/answerdev/answer/internal/service/report"
"github.com/gin-gonic/gin"
@@ -40,7 +41,7 @@ func (rc *ReportController) AddReport(ctx *gin.Context) {
}
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
- can, err := rc.rankService.CheckOperationPermission(ctx, req.UserID, rank.ReportAddRank, "")
+ can, err := rc.rankService.CheckOperationPermission(ctx, req.UserID, permission.ReportAdd, "")
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
diff --git a/internal/controller/revision_controller.go b/internal/controller/revision_controller.go
index 034f343d..1327cb80 100644
--- a/internal/controller/revision_controller.go
+++ b/internal/controller/revision_controller.go
@@ -7,6 +7,7 @@ import (
"github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service"
+ "github.com/answerdev/answer/internal/service/permission"
"github.com/answerdev/answer/internal/service/rank"
"github.com/answerdev/answer/pkg/obj"
"github.com/gin-gonic/gin"
@@ -70,10 +71,10 @@ func (rc *RevisionController) GetUnreviewedRevisionList(ctx *gin.Context) {
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
canList, err := rc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{
- rank.QuestionAuditRank,
- rank.AnswerAuditRank,
- rank.TagAuditRank,
- }, "")
+ permission.QuestionAudit,
+ permission.AnswerAudit,
+ permission.TagAudit,
+ })
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
@@ -102,10 +103,10 @@ func (rc *RevisionController) RevisionAudit(ctx *gin.Context) {
}
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
canList, err := rc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{
- rank.QuestionAuditRank,
- rank.AnswerAuditRank,
- rank.TagAuditRank,
- }, "")
+ permission.QuestionAudit,
+ permission.AnswerAudit,
+ permission.TagAudit,
+ })
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
@@ -139,11 +140,11 @@ func (rc *RevisionController) CheckCanUpdateRevision(ctx *gin.Context) {
objectTypeStr, _ := obj.GetObjectTypeStrByObjectID(req.ID)
switch objectTypeStr {
case constant.QuestionObjectType:
- action = rank.QuestionEditRank
+ action = permission.QuestionEdit
case constant.AnswerObjectType:
- action = rank.AnswerEditRank
+ action = permission.AnswerEdit
case constant.TagObjectType:
- action = rank.TagEditRank
+ action = permission.TagEdit
default:
handler.HandleResponse(ctx, errors.BadRequest(reason.ObjectNotFound), nil)
return
diff --git a/internal/controller/siteinfo_controller.go b/internal/controller/siteinfo_controller.go
index 80d6faa7..732a2091 100644
--- a/internal/controller/siteinfo_controller.go
+++ b/internal/controller/siteinfo_controller.go
@@ -1,6 +1,9 @@
package controller
import (
+ "net/http"
+
+ "github.com/answerdev/answer/internal/base/constant"
"github.com/answerdev/answer/internal/base/handler"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/siteinfo_common"
@@ -24,7 +27,7 @@ func NewSiteinfoController(siteInfoService *siteinfo_common.SiteInfoCommonServic
// @Description get site info
// @Tags site
// @Produce json
-// @Success 200 {object} handler.RespBody{data=schema.SiteGeneralResp}
+// @Success 200 {object} handler.RespBody{data=schema.SiteInfoResp}
// @Router /answer/api/v1/siteinfo [get]
func (sc *SiteinfoController) GetSiteInfo(ctx *gin.Context) {
var err error
@@ -42,6 +45,26 @@ func (sc *SiteinfoController) GetSiteInfo(ctx *gin.Context) {
if err != nil {
log.Error(err)
}
+
+ resp.Login, err = sc.siteInfoService.GetSiteLogin(ctx)
+ if err != nil {
+ log.Error(err)
+ }
+
+ resp.Theme, err = sc.siteInfoService.GetSiteTheme(ctx)
+ if err != nil {
+ log.Error(err)
+ }
+
+ resp.CustomCssHtml, err = sc.siteInfoService.GetSiteCustomCssHTML(ctx)
+ if err != nil {
+ log.Error(err)
+ }
+ resp.SiteSeo, err = sc.siteInfoService.GetSiteSeo(ctx)
+ if err != nil {
+ log.Error(err)
+ }
+
handler.HandleResponse(ctx, nil, resp)
}
@@ -73,3 +96,34 @@ func (sc *SiteinfoController) GetSiteLegalInfo(ctx *gin.Context) {
}
handler.HandleResponse(ctx, nil, resp)
}
+
+// GetManifestJson get manifest.json
+func (sc *SiteinfoController) GetManifestJson(ctx *gin.Context) {
+ favicon := "favicon.ico"
+ resp := &schema.GetManifestJsonResp{
+ ManifestVersion: 3,
+ Version: constant.Version,
+ ShortName: "Answer",
+ Name: "Answer.dev",
+ Icons: map[string]string{
+ "16": favicon,
+ "32": favicon,
+ "48": favicon,
+ "128": favicon,
+ },
+ StartUrl: ".",
+ Display: "standalone",
+ ThemeColor: "#000000",
+ BackgroundColor: "#ffffff",
+ }
+ branding, err := sc.siteInfoService.GetSiteBranding(ctx)
+ if err != nil {
+ log.Error(err)
+ } else if len(branding.Favicon) > 0 {
+ resp.Icons["16"] = branding.Favicon
+ resp.Icons["32"] = branding.Favicon
+ resp.Icons["48"] = branding.Favicon
+ resp.Icons["128"] = branding.Favicon
+ }
+ ctx.JSON(http.StatusOK, resp)
+}
diff --git a/internal/controller/tag_controller.go b/internal/controller/tag_controller.go
index 62af3888..35b3f69f 100644
--- a/internal/controller/tag_controller.go
+++ b/internal/controller/tag_controller.go
@@ -5,6 +5,7 @@ import (
"github.com/answerdev/answer/internal/base/middleware"
"github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/internal/schema"
+ "github.com/answerdev/answer/internal/service/permission"
"github.com/answerdev/answer/internal/service/rank"
"github.com/answerdev/answer/internal/service/tag"
"github.com/answerdev/answer/internal/service/tag_common"
@@ -63,7 +64,7 @@ func (tc *TagController) RemoveTag(ctx *gin.Context) {
}
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
- can, err := tc.rankService.CheckOperationPermission(ctx, req.UserID, rank.TagDeleteRank, "")
+ can, err := tc.rankService.CheckOperationPermission(ctx, req.UserID, permission.TagDelete, "")
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
@@ -94,9 +95,9 @@ func (tc *TagController) UpdateTag(ctx *gin.Context) {
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
canList, err := tc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{
- rank.TagEditRank,
- rank.TagEditWithoutReviewRank,
- }, "")
+ permission.TagEdit,
+ permission.TagEditWithoutReview,
+ })
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
@@ -133,9 +134,9 @@ func (tc *TagController) GetTagInfo(ctx *gin.Context) {
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
canList, err := tc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{
- rank.TagEditRank,
- rank.TagDeleteRank,
- }, "")
+ permission.TagEdit,
+ permission.TagDelete,
+ })
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
@@ -199,14 +200,12 @@ func (tc *TagController) GetTagSynonyms(ctx *gin.Context) {
}
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
- canList, err := tc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{
- rank.TagSynonymRank,
- }, "")
+ can, err := tc.rankService.CheckOperationPermission(ctx, req.UserID, permission.TagSynonym, "")
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
}
- req.CanEdit = canList[0]
+ req.CanEdit = can
resp, err := tc.tagService.GetTagSynonyms(ctx, req)
handler.HandleResponse(ctx, err, resp)
@@ -228,7 +227,7 @@ func (tc *TagController) UpdateTagSynonym(ctx *gin.Context) {
}
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
- can, err := tc.rankService.CheckOperationPermission(ctx, req.UserID, rank.TagSynonymRank, "")
+ can, err := tc.rankService.CheckOperationPermission(ctx, req.UserID, permission.TagSynonym, "")
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
diff --git a/internal/controller/template_controller.go b/internal/controller/template_controller.go
new file mode 100644
index 00000000..cf02268c
--- /dev/null
+++ b/internal/controller/template_controller.go
@@ -0,0 +1,487 @@
+package controller
+
+import (
+ "encoding/json"
+ "fmt"
+ "html/template"
+ "net/http"
+ "regexp"
+ "strings"
+ "time"
+
+ "github.com/answerdev/answer/internal/base/constant"
+ "github.com/answerdev/answer/internal/base/handler"
+ templaterender "github.com/answerdev/answer/internal/controller/template_render"
+ "github.com/answerdev/answer/internal/schema"
+ "github.com/answerdev/answer/internal/service/siteinfo_common"
+ "github.com/answerdev/answer/pkg/converter"
+ "github.com/answerdev/answer/pkg/htmltext"
+ "github.com/answerdev/answer/pkg/obj"
+ "github.com/answerdev/answer/ui"
+ "github.com/gin-gonic/gin"
+ "github.com/segmentfault/pacman/log"
+)
+
+type TemplateController struct {
+ scriptPath string
+ cssPath string
+ templateRenderController *templaterender.TemplateRenderController
+ siteInfoService *siteinfo_common.SiteInfoCommonService
+}
+
+// NewTemplateController new controller
+func NewTemplateController(
+ templateRenderController *templaterender.TemplateRenderController,
+ siteInfoService *siteinfo_common.SiteInfoCommonService,
+) *TemplateController {
+ script, css := GetStyle()
+ return &TemplateController{
+ scriptPath: script,
+ cssPath: css,
+ templateRenderController: templateRenderController,
+ siteInfoService: siteInfoService,
+ }
+}
+func GetStyle() (script, css string) {
+ file, err := ui.Build.ReadFile("build/index.html")
+ if err != nil {
+ return
+ }
+ scriptRegexp := regexp.MustCompile(``)
+ scriptData := scriptRegexp.FindStringSubmatch(string(file))
+ cssRegexp := regexp.MustCompile(``)
+ cssListData := cssRegexp.FindStringSubmatch(string(file))
+ if len(scriptData) == 2 {
+ script = scriptData[1]
+ }
+ if len(cssListData) == 2 {
+ css = cssListData[1]
+ }
+ return
+}
+func (tc *TemplateController) SiteInfo(ctx *gin.Context) *schema.TemplateSiteInfoResp {
+ var err error
+ resp := &schema.TemplateSiteInfoResp{}
+ resp.General, err = tc.siteInfoService.GetSiteGeneral(ctx)
+ if err != nil {
+ log.Error(err)
+ }
+ resp.Interface, err = tc.siteInfoService.GetSiteInterface(ctx)
+ if err != nil {
+ log.Error(err)
+ }
+
+ resp.Branding, err = tc.siteInfoService.GetSiteBranding(ctx)
+ if err != nil {
+ log.Error(err)
+ }
+
+ resp.SiteSeo, err = tc.siteInfoService.GetSiteSeo(ctx)
+ if err != nil {
+ log.Error(err)
+ }
+
+ resp.CustomCssHtml, err = tc.siteInfoService.GetSiteCustomCssHTML(ctx)
+ if err != nil {
+ log.Error(err)
+ }
+ resp.Year = fmt.Sprintf("%d", time.Now().Year())
+ return resp
+}
+
+// Index question list
+func (tc *TemplateController) Index(ctx *gin.Context) {
+ req := &schema.QuestionSearch{
+ Order: "newest",
+ }
+ if handler.BindAndCheck(ctx, req) {
+ tc.Page404(ctx)
+ return
+ }
+
+ var page = req.Page
+
+ data, count, err := tc.templateRenderController.Index(ctx, req)
+ if err != nil {
+ tc.Page404(ctx)
+ return
+ }
+
+ siteInfo := tc.SiteInfo(ctx)
+ siteInfo.Canonical = siteInfo.General.SiteUrl
+
+ UrlUseTitle := false
+ if siteInfo.SiteSeo.PermaLink == schema.PermaLinkQuestionIDAndTitle {
+ UrlUseTitle = true
+ }
+ siteInfo.Title = ""
+ tc.html(ctx, http.StatusOK, "question.html", siteInfo, gin.H{
+ "data": data,
+ "useTitle": UrlUseTitle,
+ "page": templaterender.Paginator(page, req.PageSize, count),
+ "path": "questions",
+ })
+}
+
+func (tc *TemplateController) QuestionList(ctx *gin.Context) {
+ req := &schema.QuestionSearch{
+ Order: "newest",
+ }
+ if handler.BindAndCheck(ctx, req) {
+ tc.Page404(ctx)
+ return
+ }
+ var page = req.Page
+ data, count, err := tc.templateRenderController.Index(ctx, req)
+ if err != nil {
+ tc.Page404(ctx)
+ return
+ }
+ siteInfo := tc.SiteInfo(ctx)
+ siteInfo.Canonical = fmt.Sprintf("%s/questions", siteInfo.General.SiteUrl)
+
+ UrlUseTitle := false
+ if siteInfo.SiteSeo.PermaLink == schema.PermaLinkQuestionIDAndTitle {
+ UrlUseTitle = true
+ }
+ siteInfo.Title = fmt.Sprintf("Questions - %s", siteInfo.General.Name)
+ tc.html(ctx, http.StatusOK, "question.html", siteInfo, gin.H{
+ "data": data,
+ "useTitle": UrlUseTitle,
+ "page": templaterender.Paginator(page, req.PageSize, count),
+ })
+}
+
+func (tc *TemplateController) QuestionInfo301Jump(ctx *gin.Context, siteInfo *schema.TemplateSiteInfoResp, correctTitle bool) (jump bool, url string) {
+ id := ctx.Param("id")
+ title := ctx.Param("title")
+ titleIsAnswerID := false
+
+ objectType, objectTypeerr := obj.GetObjectTypeStrByObjectID(title)
+ if objectTypeerr == nil {
+ if objectType == constant.AnswerObjectType {
+ titleIsAnswerID = true
+ }
+ }
+
+ url = fmt.Sprintf("%s/questions/%s", siteInfo.General.SiteUrl, id)
+ if siteInfo.SiteSeo.PermaLink == schema.PermaLinkQuestionID {
+ //not have title
+ if titleIsAnswerID || len(title) == 0 {
+ return false, ""
+ }
+ return true, url
+ } else {
+ //have title
+ if len(title) > 0 && !titleIsAnswerID && correctTitle {
+ return false, ""
+ }
+ detail, err := tc.templateRenderController.QuestionDetail(ctx, id)
+ if err != nil {
+ tc.Page404(ctx)
+ return
+ }
+ url = fmt.Sprintf("%s/%s", url, htmltext.UrlTitle(detail.Title))
+ return true, url
+ }
+}
+
+// QuestionInfo question and answers info
+func (tc *TemplateController) QuestionInfo(ctx *gin.Context) {
+ id := ctx.Param("id")
+ title := ctx.Param("title")
+ answerid := ctx.Param("answerid")
+
+ if id == "ask" {
+ file, err := ui.Build.ReadFile("build/index.html")
+ if err != nil {
+ log.Error(err)
+ tc.Page404(ctx)
+ return
+ }
+ ctx.Header("content-type", "text/html;charset=utf-8")
+ ctx.String(http.StatusOK, string(file))
+ return
+ }
+
+ correctTitle := false
+
+ detail, err := tc.templateRenderController.QuestionDetail(ctx, id)
+ if err != nil {
+ tc.Page404(ctx)
+ return
+ }
+ encodeTitle := htmltext.UrlTitle(detail.Title)
+ if encodeTitle == title {
+ correctTitle = true
+ }
+
+ siteInfo := tc.SiteInfo(ctx)
+ jump, jumpurl := tc.QuestionInfo301Jump(ctx, siteInfo, correctTitle)
+ if jump {
+ ctx.Redirect(http.StatusMovedPermanently, jumpurl)
+ return
+ }
+
+ // answers
+ answerReq := &schema.AnswerListReq{
+ QuestionID: id,
+ Order: "",
+ Page: 1,
+ PageSize: 999,
+ UserID: "",
+ }
+ answers, answerCount, err := tc.templateRenderController.AnswerList(ctx, answerReq)
+ if err != nil {
+ tc.Page404(ctx)
+ return
+ }
+
+ // comments
+ objectIDs := []string{id}
+ for _, answer := range answers {
+ objectIDs = append(objectIDs, answer.ID)
+ }
+ comments, err := tc.templateRenderController.CommentList(ctx, objectIDs)
+ if err != nil {
+ tc.Page404(ctx)
+ return
+ }
+ siteInfo.Canonical = fmt.Sprintf("%s/questions/%s/%s", siteInfo.General.SiteUrl, id, encodeTitle)
+ if siteInfo.SiteSeo.PermaLink == schema.PermaLinkQuestionID {
+ siteInfo.Canonical = fmt.Sprintf("%s/questions/%s", siteInfo.General.SiteUrl, id)
+ }
+ jsonLD := &schema.QAPageJsonLD{}
+ jsonLD.Context = "https://schema.org"
+ jsonLD.Type = "QAPage"
+ jsonLD.MainEntity.Type = "Question"
+ jsonLD.MainEntity.Name = detail.Title
+ jsonLD.MainEntity.Text = detail.HTML
+ jsonLD.MainEntity.AnswerCount = int(answerCount)
+ jsonLD.MainEntity.UpvoteCount = detail.VoteCount
+ jsonLD.MainEntity.DateCreated = time.Unix(detail.CreateTime, 0)
+ jsonLD.MainEntity.Author.Type = "Person"
+ jsonLD.MainEntity.Author.Name = detail.UserInfo.DisplayName
+ answerList := make([]*schema.SuggestedAnswerItem, 0)
+ for _, answer := range answers {
+ if answer.Adopted == schema.AnswerAdoptedEnable {
+ acceptedAnswerItem := &schema.AcceptedAnswerItem{}
+ acceptedAnswerItem.Type = "Answer"
+ acceptedAnswerItem.Text = answer.HTML
+ acceptedAnswerItem.DateCreated = time.Unix(answer.CreateTime, 0)
+ acceptedAnswerItem.UpvoteCount = answer.VoteCount
+ acceptedAnswerItem.URL = fmt.Sprintf("%s/%s", siteInfo.Canonical, answer.ID)
+ acceptedAnswerItem.Author.Type = "Person"
+ acceptedAnswerItem.Author.Name = answer.UserInfo.DisplayName
+ jsonLD.MainEntity.AcceptedAnswer = acceptedAnswerItem
+ } else {
+ item := &schema.SuggestedAnswerItem{}
+ item.Type = "Answer"
+ item.Text = answer.HTML
+ item.DateCreated = time.Unix(answer.CreateTime, 0)
+ item.UpvoteCount = answer.VoteCount
+ item.URL = fmt.Sprintf("%s/%s", siteInfo.Canonical, answer.ID)
+ item.Author.Type = "Person"
+ item.Author.Name = answer.UserInfo.DisplayName
+ answerList = append(answerList, item)
+ }
+
+ }
+ jsonLD.MainEntity.SuggestedAnswer = answerList
+ jsonLDStr, err := json.Marshal(jsonLD)
+ if err == nil {
+ siteInfo.JsonLD = ``
+ }
+
+ siteInfo.Description = htmltext.FetchExcerpt(detail.HTML, "...", 240)
+ tags := make([]string, 0)
+ for _, tag := range detail.Tags {
+ tags = append(tags, tag.DisplayName)
+ }
+ siteInfo.Keywords = strings.Replace(strings.Trim(fmt.Sprint(tags), "[]"), " ", ",", -1)
+ siteInfo.Title = fmt.Sprintf("%s - %s", detail.Title, siteInfo.General.Name)
+ tc.html(ctx, http.StatusOK, "question-detail.html", siteInfo, gin.H{
+ "id": id,
+ "answerid": answerid,
+ "detail": detail,
+ "answers": answers,
+ "comments": comments,
+ })
+}
+
+// TagList tags list
+func (tc *TemplateController) TagList(ctx *gin.Context) {
+ req := &schema.GetTagWithPageReq{}
+ if handler.BindAndCheck(ctx, req) {
+ return
+ }
+ data, err := tc.templateRenderController.TagList(ctx, req)
+ if err != nil {
+ tc.Page404(ctx)
+ return
+ }
+ page := templaterender.Paginator(req.Page, req.PageSize, data.Count)
+
+ siteInfo := tc.SiteInfo(ctx)
+ siteInfo.Canonical = fmt.Sprintf("%s/tags", siteInfo.General.SiteUrl)
+ siteInfo.Title = fmt.Sprintf("%s - %s", "Tags", siteInfo.General.Name)
+ tc.html(ctx, http.StatusOK, "tags.html", siteInfo, gin.H{
+ "page": page,
+ "data": data,
+ })
+}
+
+// TagInfo taginfo
+func (tc *TemplateController) TagInfo(ctx *gin.Context) {
+ tag := ctx.Param("tag")
+ req := &schema.GetTamplateTagInfoReq{}
+ if handler.BindAndCheck(ctx, req) {
+ tc.Page404(ctx)
+ return
+ }
+ nowPage := req.Page
+ req.Name = tag
+ taginifo, questionList, questionCount, err := tc.templateRenderController.TagInfo(ctx, req)
+ if err != nil {
+ tc.Page404(ctx)
+ return
+ }
+ page := templaterender.Paginator(nowPage, req.PageSize, questionCount)
+
+ siteInfo := tc.SiteInfo(ctx)
+ siteInfo.Canonical = fmt.Sprintf("%s/tags/%s", siteInfo.General.SiteUrl, tag)
+ siteInfo.Description = htmltext.FetchExcerpt(taginifo.ParsedText, "...", 240)
+ if len(taginifo.ParsedText) == 0 {
+ siteInfo.Description = "The tag has no description."
+ }
+ siteInfo.Keywords = taginifo.DisplayName
+
+ UrlUseTitle := false
+ if siteInfo.SiteSeo.PermaLink == schema.PermaLinkQuestionIDAndTitle {
+ UrlUseTitle = true
+ }
+ siteInfo.Title = fmt.Sprintf("'%s' Questions - %s", taginifo.DisplayName, siteInfo.General.Name)
+ tc.html(ctx, http.StatusOK, "tag-detail.html", siteInfo, gin.H{
+ "tag": taginifo,
+ "questionList": questionList,
+ "questionCount": questionCount,
+ "useTitle": UrlUseTitle,
+ "page": page,
+ })
+}
+
+// UserInfo user info
+func (tc *TemplateController) UserInfo(ctx *gin.Context) {
+ username := ctx.Param("username")
+ if username == "" {
+ tc.Page404(ctx)
+ return
+ }
+ exist := constant.ExistInPathIgnore(username)
+ if exist {
+ file, err := ui.Build.ReadFile("build/index.html")
+ if err != nil {
+ log.Error(err)
+ tc.Page404(ctx)
+ return
+ }
+ ctx.Header("content-type", "text/html;charset=utf-8")
+ ctx.String(http.StatusOK, string(file))
+ return
+ }
+ req := &schema.GetOtherUserInfoByUsernameReq{}
+ req.Username = username
+ userinfo, err := tc.templateRenderController.UserInfo(ctx, req)
+
+ if err != nil {
+ tc.Page404(ctx)
+ return
+ }
+ if !userinfo.Has {
+ tc.Page404(ctx)
+ return
+ }
+
+ siteInfo := tc.SiteInfo(ctx)
+ siteInfo.Canonical = fmt.Sprintf("%s/users/%s", siteInfo.General.SiteUrl, username)
+ siteInfo.Title = fmt.Sprintf("%s - %s", username, siteInfo.General.Name)
+ tc.html(ctx, http.StatusOK, "homepage.html", siteInfo, gin.H{
+ "userinfo": userinfo,
+ "bio": template.HTML(userinfo.Info.BioHTML),
+ })
+
+}
+
+func (tc *TemplateController) Page404(ctx *gin.Context) {
+ tc.html(ctx, http.StatusNotFound, "404.html", tc.SiteInfo(ctx), gin.H{})
+}
+
+func (tc *TemplateController) html(ctx *gin.Context, code int, tpl string, siteInfo *schema.TemplateSiteInfoResp, data gin.H) {
+ data["siteinfo"] = siteInfo
+ data["scriptPath"] = tc.scriptPath
+ data["cssPath"] = tc.cssPath
+ data["keywords"] = siteInfo.Keywords
+ if siteInfo.Description == "" {
+ siteInfo.Description = siteInfo.General.Description
+ }
+ data["title"] = siteInfo.Title
+ if siteInfo.Title == "" {
+ data["title"] = siteInfo.General.Name
+ }
+ data["description"] = siteInfo.Description
+ data["language"] = handler.GetLang(ctx)
+ data["timezone"] = siteInfo.Interface.TimeZone
+ data["HeadCode"] = siteInfo.CustomCssHtml.CustomHead
+ data["HeaderCode"] = siteInfo.CustomCssHtml.CustomHeader
+ data["FooterCode"] = siteInfo.CustomCssHtml.CustomFooter
+ _, ok := data["path"]
+ if !ok {
+ data["path"] = ""
+ }
+ ctx.HTML(code, tpl, data)
+}
+
+func (tc *TemplateController) Sitemap(ctx *gin.Context) {
+ if tc.checkPrivateMode(ctx) {
+ tc.Page404(ctx)
+ return
+ }
+ tc.templateRenderController.Sitemap(ctx)
+}
+
+func (tc *TemplateController) SitemapPage(ctx *gin.Context) {
+ if tc.checkPrivateMode(ctx) {
+ tc.Page404(ctx)
+ return
+ }
+ page := 0
+ pageParam := ctx.Param("page")
+ pageRegexp := regexp.MustCompile(`question-(.*).xml`)
+ pageStr := pageRegexp.FindStringSubmatch(pageParam)
+ if len(pageStr) != 2 {
+ tc.Page404(ctx)
+ return
+ }
+ page = converter.StringToInt(pageStr[1])
+ if page == 0 {
+ tc.Page404(ctx)
+ return
+ }
+ err := tc.templateRenderController.SitemapPage(ctx, page)
+ if err != nil {
+ tc.Page404(ctx)
+ return
+ }
+}
+
+func (tc *TemplateController) checkPrivateMode(ctx *gin.Context) bool {
+ resp, err := tc.siteInfoService.GetSiteLogin(ctx)
+ if err != nil {
+ log.Error(err)
+ return false
+ }
+ if resp.LoginRequired {
+ return true
+ }
+ return false
+}
diff --git a/internal/controller/template_render/answer.go b/internal/controller/template_render/answer.go
new file mode 100644
index 00000000..7893c108
--- /dev/null
+++ b/internal/controller/template_render/answer.go
@@ -0,0 +1,11 @@
+package templaterender
+
+import (
+ "context"
+
+ "github.com/answerdev/answer/internal/schema"
+)
+
+func (t *TemplateRenderController) AnswerList(ctx context.Context, req *schema.AnswerListReq) ([]*schema.AnswerInfo, int64, error) {
+ return t.answerService.SearchList(ctx, req)
+}
diff --git a/internal/controller/template_render/comment.go b/internal/controller/template_render/comment.go
new file mode 100644
index 00000000..dfdd81a6
--- /dev/null
+++ b/internal/controller/template_render/comment.go
@@ -0,0 +1,38 @@
+package templaterender
+
+import (
+ "context"
+ "github.com/answerdev/answer/internal/base/pager"
+ "github.com/answerdev/answer/internal/schema"
+)
+
+func (t *TemplateRenderController) CommentList(
+ ctx context.Context,
+ objectIDs []string,
+) (
+ comments map[string][]*schema.GetCommentResp,
+ err error,
+) {
+
+ comments = make(map[string][]*schema.GetCommentResp, len(objectIDs))
+
+ for _, objectID := range objectIDs {
+ var (
+ req = &schema.GetCommentWithPageReq{
+ Page: 1,
+ PageSize: 3,
+ ObjectID: objectID,
+ QueryCond: "vote",
+ UserID: "",
+ }
+ pageModel *pager.PageModel
+ )
+ pageModel, err = t.commentService.GetCommentWithPage(ctx, req)
+ if err != nil {
+ return
+ }
+ li := pageModel.List
+ comments[objectID] = li.([]*schema.GetCommentResp)
+ }
+ return
+}
diff --git a/internal/controller/template_render/controller.go b/internal/controller/template_render/controller.go
new file mode 100644
index 00000000..f44c1ea6
--- /dev/null
+++ b/internal/controller/template_render/controller.go
@@ -0,0 +1,106 @@
+package templaterender
+
+import (
+ "math"
+
+ "github.com/answerdev/answer/internal/base/data"
+ "github.com/answerdev/answer/internal/service/comment"
+ "github.com/answerdev/answer/internal/service/siteinfo_common"
+
+ "github.com/answerdev/answer/internal/schema"
+ "github.com/answerdev/answer/internal/service"
+ "github.com/answerdev/answer/internal/service/tag"
+ "github.com/google/wire"
+)
+
+// ProviderSetTemplateRenderController is template render controller providers.
+var ProviderSetTemplateRenderController = wire.NewSet(
+ NewTemplateRenderController,
+)
+
+type TemplateRenderController struct {
+ questionService *service.QuestionService
+ userService *service.UserService
+ tagService *tag.TagService
+ answerService *service.AnswerService
+ commentService *comment.CommentService
+ data *data.Data
+ siteInfoService *siteinfo_common.SiteInfoCommonService
+}
+
+func NewTemplateRenderController(
+ questionService *service.QuestionService,
+ userService *service.UserService,
+ tagService *tag.TagService,
+ answerService *service.AnswerService,
+ commentService *comment.CommentService,
+ data *data.Data,
+ siteInfoService *siteinfo_common.SiteInfoCommonService,
+
+) *TemplateRenderController {
+ return &TemplateRenderController{
+ questionService: questionService,
+ userService: userService,
+ tagService: tagService,
+ answerService: answerService,
+ commentService: commentService,
+ data: data,
+ siteInfoService: siteInfoService,
+ }
+}
+
+// Paginator page
+// page : now page
+// pageSize : Number per page
+// nums : Total
+// Returns the contents of the page in the format of 1, 2, 3, 4, and 5. If the contents are less than 5 pages, the page number is returned
+func Paginator(page, pageSize int, nums int64) *schema.Paginator {
+ if pageSize == 0 {
+ pageSize = 10
+ }
+
+ var prevpage int //Previous page address
+ var nextpage int //Address on the last page
+ //Generate the total number of pages based on the total number of nums and the number of prepage pages
+ totalpages := int(math.Ceil(float64(nums) / float64(pageSize))) //Total number of Pages
+ if page > totalpages {
+ page = totalpages
+ }
+ if page <= 0 {
+ page = 1
+ }
+ var pages []int
+ switch {
+ case page >= totalpages-5 && totalpages > 5: //The last 5 pages
+ start := totalpages - 5 + 1
+ prevpage = page - 1
+ nextpage = int(math.Min(float64(totalpages), float64(page+1)))
+ pages = make([]int, 5)
+ for i := range pages {
+ pages[i] = start + i
+ }
+ case page >= 3 && totalpages > 5:
+ start := page - 3 + 1
+ pages = make([]int, 5)
+ prevpage = page - 3
+ for i := range pages {
+ pages[i] = start + i
+ }
+ prevpage = page - 1
+ nextpage = page + 1
+ default:
+ pages = make([]int, int(math.Min(5, float64(totalpages))))
+ for i := range pages {
+ pages[i] = i + 1
+ }
+ prevpage = int(math.Max(float64(1), float64(page-1)))
+ nextpage = page + 1
+ }
+ paginator := &schema.Paginator{}
+ paginator.Pages = pages
+ paginator.Totalpages = totalpages
+ paginator.Prevpage = prevpage
+ paginator.Nextpage = nextpage
+ paginator.Currpage = page
+ return paginator
+}
diff --git a/internal/controller/template_render/index.go b/internal/controller/template_render/index.go
new file mode 100644
index 00000000..362739ec
--- /dev/null
+++ b/internal/controller/template_render/index.go
@@ -0,0 +1 @@
+package templaterender
diff --git a/internal/controller/template_render/question.go b/internal/controller/template_render/question.go
new file mode 100644
index 00000000..5025979c
--- /dev/null
+++ b/internal/controller/template_render/question.go
@@ -0,0 +1,91 @@
+package templaterender
+
+import (
+ "encoding/json"
+ "fmt"
+ "html/template"
+ "net/http"
+
+ "github.com/answerdev/answer/internal/schema"
+ "github.com/gin-gonic/gin"
+ "github.com/segmentfault/pacman/log"
+)
+
+func (t *TemplateRenderController) Index(ctx *gin.Context, req *schema.QuestionSearch) ([]*schema.QuestionInfo, int64, error) {
+ return t.questionService.SearchList(ctx, req, req.UserID)
+}
+
+func (t *TemplateRenderController) QuestionDetail(ctx *gin.Context, id string) (resp *schema.QuestionInfo, err error) {
+ return t.questionService.GetQuestion(ctx, id, "", schema.QuestionPermission{})
+}
+
+func (t *TemplateRenderController) Sitemap(ctx *gin.Context) {
+ general, err := t.siteInfoService.GetSiteGeneral(ctx)
+ if err != nil {
+ log.Error("get site general failed:", err)
+ return
+ }
+
+ sitemapInfo := &schema.SiteMapList{}
+ infoStr, err := t.data.Cache.GetString(ctx, schema.SitemapCachekey)
+ if err != nil {
+ log.Errorf("get Cache failed: %s", err)
+ return
+ }
+ if err = json.Unmarshal([]byte(infoStr), sitemapInfo); err != nil {
+ log.Errorf("get sitemap info failed: %s", err)
+ return
+ }
+
+ if len(sitemapInfo.QuestionIDs) > 0 {
+ //question url list
+ ctx.Header("Content-Type", "application/xml")
+ ctx.HTML(
+ http.StatusOK, "sitemap.xml", gin.H{
+ "xmlHeader": template.HTML(``),
+ "list": sitemapInfo.QuestionIDs,
+ "general": general,
+ },
+ )
+ } else {
+ //question list page
+ ctx.Header("Content-Type", "application/xml")
+ ctx.HTML(
+ http.StatusOK, "sitemap-list.xml", gin.H{
+ "xmlHeader": template.HTML(``),
+ "page": sitemapInfo.MaxPageNum,
+ "general": general,
+ },
+ )
+ return
+ }
+}
+
+func (t *TemplateRenderController) SitemapPage(ctx *gin.Context, page int) error {
+ sitemapInfo := &schema.SiteMapPageList{}
+ general, err := t.siteInfoService.GetSiteGeneral(ctx)
+ if err != nil {
+ log.Error("get site general failed:", err)
+ return err
+ }
+
+ cachekey := fmt.Sprintf(schema.SitemapPageCachekey, page)
+ infoStr, err := t.data.Cache.GetString(ctx, cachekey)
+ if err != nil {
+ //If there is no cache, return directly.
+ return nil
+ }
+ if err = json.Unmarshal([]byte(infoStr), sitemapInfo); err != nil {
+ log.Errorf("get sitemap info failed: %s", err)
+ return err
+ }
+ ctx.Header("Content-Type", "application/xml")
+ ctx.HTML(
+ http.StatusOK, "sitemap.xml", gin.H{
+ "xmlHeader": template.HTML(``),
+ "list": sitemapInfo.PageData,
+ "general": general,
+ },
+ )
+ return nil
+}
diff --git a/internal/controller/template_render/tags.go b/internal/controller/template_render/tags.go
new file mode 100644
index 00000000..d8a36b64
--- /dev/null
+++ b/internal/controller/template_render/tags.go
@@ -0,0 +1,35 @@
+package templaterender
+
+import (
+ "github.com/answerdev/answer/internal/base/pager"
+ "github.com/answerdev/answer/internal/schema"
+ "github.com/jinzhu/copier"
+ "golang.org/x/net/context"
+)
+
+func (q *TemplateRenderController) TagList(ctx context.Context, req *schema.GetTagWithPageReq) (resp *pager.PageModel, err error) {
+ resp, err = q.tagService.GetTagWithPage(ctx, req)
+ if err != nil {
+ return
+ }
+ return
+}
+
+func (q *TemplateRenderController) TagInfo(ctx context.Context, req *schema.GetTamplateTagInfoReq) (resp *schema.GetTagResp, questionList []*schema.QuestionInfo, questionCount int64, err error) {
+ dto := &schema.GetTagInfoReq{}
+ _ = copier.Copy(dto, req)
+ resp, err = q.tagService.GetTagInfo(ctx, dto)
+ if err != nil {
+ return
+ }
+ searchQuestion := &schema.QuestionSearch{}
+ searchQuestion.Page = req.Page
+ searchQuestion.PageSize = req.PageSize
+ searchQuestion.Order = "newest"
+ searchQuestion.Tag = req.Name
+ questionList, questionCount, err = q.questionService.SearchList(ctx, searchQuestion, "")
+ if err != nil {
+ return
+ }
+ return resp, questionList, questionCount, err
+}
diff --git a/internal/controller/template_render/userinfo.go b/internal/controller/template_render/userinfo.go
new file mode 100644
index 00000000..e10e2dfa
--- /dev/null
+++ b/internal/controller/template_render/userinfo.go
@@ -0,0 +1,10 @@
+package templaterender
+
+import (
+ "github.com/answerdev/answer/internal/schema"
+ "golang.org/x/net/context"
+)
+
+func (q *TemplateRenderController) UserInfo(ctx context.Context, req *schema.GetOtherUserInfoByUsernameReq) (resp *schema.GetOtherUserInfoResp, err error) {
+ return q.userService.GetOtherUserInfoByUsername(ctx, req.Username)
+}
diff --git a/internal/controller/user_controller.go b/internal/controller/user_controller.go
index 099adc42..bb9c9796 100644
--- a/internal/controller/user_controller.go
+++ b/internal/controller/user_controller.go
@@ -11,18 +11,21 @@ import (
"github.com/answerdev/answer/internal/service/action"
"github.com/answerdev/answer/internal/service/auth"
"github.com/answerdev/answer/internal/service/export"
+ "github.com/answerdev/answer/internal/service/siteinfo_common"
"github.com/answerdev/answer/internal/service/uploader"
"github.com/gin-gonic/gin"
"github.com/segmentfault/pacman/errors"
+ "github.com/segmentfault/pacman/log"
)
// UserController user controller
type UserController struct {
- userService *service.UserService
- authService *auth.AuthService
- actionService *action.CaptchaService
- uploaderService *uploader.UploaderService
- emailService *export.EmailService
+ userService *service.UserService
+ authService *auth.AuthService
+ actionService *action.CaptchaService
+ uploaderService *uploader.UploaderService
+ emailService *export.EmailService
+ siteInfoCommonService *siteinfo_common.SiteInfoCommonService
}
// NewUserController new controller
@@ -32,13 +35,15 @@ func NewUserController(
actionService *action.CaptchaService,
emailService *export.EmailService,
uploaderService *uploader.UploaderService,
+ siteInfoCommonService *siteinfo_common.SiteInfoCommonService,
) *UserController {
return &UserController{
- authService: authService,
- userService: userService,
- actionService: actionService,
- uploaderService: uploaderService,
- emailService: emailService,
+ authService: authService,
+ userService: userService,
+ actionService: actionService,
+ uploaderService: uploaderService,
+ emailService: emailService,
+ siteInfoCommonService: siteInfoCommonService,
}
}
@@ -52,16 +57,20 @@ func NewUserController(
// @Success 200 {object} handler.RespBody{data=schema.GetUserToSetShowResp}
// @Router /answer/api/v1/user/info [get]
func (uc *UserController) GetUserInfoByUserID(ctx *gin.Context) {
- userID := middleware.GetLoginUserIDFromContext(ctx)
token := middleware.ExtractToken(ctx)
-
- // if user is no login return null in data
- if len(token) == 0 || len(userID) == 0 {
+ if len(token) == 0 {
handler.HandleResponse(ctx, nil, nil)
return
}
- resp, err := uc.userService.GetUserInfoByUserID(ctx, token, userID)
+ // if user is no login return null in data
+ userInfo, _ := uc.authService.GetUserCacheInfo(ctx, token)
+ if userInfo == nil {
+ handler.HandleResponse(ctx, nil, nil)
+ return
+ }
+
+ resp, err := uc.userService.GetUserInfoByUserID(ctx, token, userInfo.UserID)
handler.HandleResponse(ctx, err, resp)
}
@@ -189,6 +198,10 @@ func (uc *UserController) UseRePassWord(ctx *gin.Context) {
// @Router /answer/api/v1/user/logout [get]
func (uc *UserController) UserLogout(ctx *gin.Context) {
accessToken := middleware.ExtractToken(ctx)
+ if len(accessToken) == 0 {
+ handler.HandleResponse(ctx, nil, nil)
+ return
+ }
_ = uc.authService.RemoveUserCacheInfo(ctx, accessToken)
handler.HandleResponse(ctx, nil, nil)
}
@@ -203,11 +216,31 @@ func (uc *UserController) UserLogout(ctx *gin.Context) {
// @Success 200 {object} handler.RespBody{data=schema.GetUserResp}
// @Router /answer/api/v1/user/register/email [post]
func (uc *UserController) UserRegisterByEmail(ctx *gin.Context) {
+ // check whether site allow register or not
+ siteInfo, err := uc.siteInfoCommonService.GetSiteLogin(ctx)
+ if err != nil {
+ handler.HandleResponse(ctx, err, nil)
+ return
+ }
+ if !siteInfo.AllowNewRegistrations {
+ handler.HandleResponse(ctx, errors.BadRequest(reason.NotAllowedRegistration), nil)
+ return
+ }
+
req := &schema.UserRegisterReq{}
if handler.BindAndCheck(ctx, req) {
return
}
req.IP = ctx.ClientIP()
+ captchaPass := uc.actionService.UserRegisterVerifyCaptcha(ctx, req.CaptchaID, req.CaptchaCode)
+ if !captchaPass {
+ errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{
+ ErrorField: "captcha_code",
+ ErrorMsg: translator.GlobalTrans.Tr(handler.GetLang(ctx), reason.CaptchaVerificationFailed),
+ })
+ handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields)
+ return
+ }
resp, err := uc.userService.UserRegisterByEmail(ctx, req)
handler.HandleResponse(ctx, err, resp)
@@ -275,11 +308,13 @@ func (uc *UserController) UserVerifyEmailSend(ctx *gin.Context) {
ErrorMsg: translator.GlobalTrans.Tr(handler.GetLang(ctx), reason.CaptchaVerificationFailed),
})
handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields)
-
return
}
- uc.actionService.ActionRecordAdd(ctx, schema.ActionRecordTypeEmail, ctx.ClientIP())
- err := uc.userService.UserVerifyEmailSend(ctx, userInfo.UserID)
+ _, err := uc.actionService.ActionRecordAdd(ctx, schema.ActionRecordTypeEmail, ctx.ClientIP())
+ if err != nil {
+ log.Error(err)
+ }
+ err = uc.userService.UserVerifyEmailSend(ctx, userInfo.UserID)
handler.HandleResponse(ctx, err, nil)
}
@@ -386,6 +421,19 @@ func (uc *UserController) ActionRecord(ctx *gin.Context) {
handler.HandleResponse(ctx, err, resp)
}
+// UserRegisterCaptcha godoc
+// @Summary UserRegisterCaptcha
+// @Description UserRegisterCaptcha
+// @Tags User
+// @Accept json
+// @Produce json
+// @Success 200 {object} handler.RespBody{data=schema.GetUserResp}
+// @Router /answer/api/v1/user/register/captcha [get]
+func (uc *UserController) UserRegisterCaptcha(ctx *gin.Context) {
+ resp, err := uc.actionService.UserRegisterCaptcha(ctx)
+ handler.HandleResponse(ctx, err, resp)
+}
+
// UserNoticeSet godoc
// @Summary UserNoticeSet
// @Description UserNoticeSet
@@ -473,3 +521,17 @@ func (uc *UserController) UserChangeEmailVerify(ctx *gin.Context) {
uc.actionService.ActionRecordDel(ctx, schema.ActionRecordTypeEmail, ctx.ClientIP())
handler.HandleResponse(ctx, err, nil)
}
+
+// UserRanking get user ranking
+// @Summary get user ranking
+// @Description get user ranking
+// @Tags User
+// @Accept json
+// @Produce json
+// @Security ApiKeyAuth
+// @Success 200 {object} handler.RespBody{data=schema.UserRankingResp}
+// @Router /answer/api/v1/user/ranking [get]
+func (uc *UserController) UserRanking(ctx *gin.Context) {
+ resp, err := uc.userService.UserRanking(ctx)
+ handler.HandleResponse(ctx, err, resp)
+}
diff --git a/internal/controller_backyard/controller.go b/internal/controller_backyard/controller.go
index 4c541f36..03b4fae1 100644
--- a/internal/controller_backyard/controller.go
+++ b/internal/controller_backyard/controller.go
@@ -8,4 +8,5 @@ var ProviderSetController = wire.NewSet(
NewUserBackyardController,
NewThemeController,
NewSiteInfoController,
+ NewRoleController,
)
diff --git a/internal/controller_backyard/role_controller.go b/internal/controller_backyard/role_controller.go
new file mode 100644
index 00000000..74d61c1b
--- /dev/null
+++ b/internal/controller_backyard/role_controller.go
@@ -0,0 +1,34 @@
+package controller_backyard
+
+import (
+ "github.com/answerdev/answer/internal/base/handler"
+ "github.com/answerdev/answer/internal/schema"
+ service "github.com/answerdev/answer/internal/service/role"
+ "github.com/gin-gonic/gin"
+)
+
+// RoleController role controller
+type RoleController struct {
+ roleService *service.RoleService
+}
+
+// NewRoleController new controller
+func NewRoleController(roleService *service.RoleService) *RoleController {
+ return &RoleController{roleService: roleService}
+}
+
+// GetRoleList get role list
+// @Summary get role list
+// @Description get role list
+// @Tags admin
+// @Produce json
+// @Success 200 {object} handler.RespBody{data=[]schema.GetRoleResp}
+// @Router /answer/admin/api/roles [get]
+func (rc *RoleController) GetRoleList(ctx *gin.Context) {
+ req := &schema.GetRoleResp{}
+ if handler.BindAndCheck(ctx, req) {
+ return
+ }
+ resp, err := rc.roleService.GetRoleList(ctx)
+ handler.HandleResponse(ctx, err, resp)
+}
diff --git a/internal/controller_backyard/siteinfo_controller.go b/internal/controller_backyard/siteinfo_controller.go
index 0950c225..79a3e922 100644
--- a/internal/controller_backyard/siteinfo_controller.go
+++ b/internal/controller_backyard/siteinfo_controller.go
@@ -1,6 +1,8 @@
package controller_backyard
import (
+ "net/http"
+
"github.com/answerdev/answer/internal/base/handler"
"github.com/answerdev/answer/internal/base/middleware"
"github.com/answerdev/answer/internal/schema"
@@ -85,6 +87,109 @@ func (sc *SiteInfoController) GetSiteLegal(ctx *gin.Context) {
handler.HandleResponse(ctx, err, resp)
}
+// GetSeo get site seo information
+// @Summary get site seo information
+// @Description get site seo information
+// @Security ApiKeyAuth
+// @Tags admin
+// @Produce json
+// @Success 200 {object} handler.RespBody{data=schema.SiteSeoResp}
+// @Router /answer/admin/api/siteinfo/seo [get]
+func (sc *SiteInfoController) GetSeo(ctx *gin.Context) {
+ resp, err := sc.siteInfoService.GetSeo(ctx)
+ handler.HandleResponse(ctx, err, resp)
+}
+
+// GetSiteLogin get site info login config
+// @Summary get site info login config
+// @Description get site info login config
+// @Security ApiKeyAuth
+// @Tags admin
+// @Produce json
+// @Success 200 {object} handler.RespBody{data=schema.SiteLoginResp}
+// @Router /answer/admin/api/siteinfo/login [get]
+func (sc *SiteInfoController) GetSiteLogin(ctx *gin.Context) {
+ resp, err := sc.siteInfoService.GetSiteLogin(ctx)
+ handler.HandleResponse(ctx, err, resp)
+}
+
+// GetSiteCustomCssHTML get site info custom html css config
+// @Summary get site info custom html css config
+// @Description get site info custom html css config
+// @Security ApiKeyAuth
+// @Tags admin
+// @Produce json
+// @Success 200 {object} handler.RespBody{data=schema.SiteCustomCssHTMLResp}
+// @Router /answer/admin/api/siteinfo/custom-css-html [get]
+func (sc *SiteInfoController) GetSiteCustomCssHTML(ctx *gin.Context) {
+ resp, err := sc.siteInfoService.GetSiteCustomCssHTML(ctx)
+ handler.HandleResponse(ctx, err, resp)
+}
+
+// GetSiteTheme get site info theme config
+// @Summary get site info theme config
+// @Description get site info theme config
+// @Security ApiKeyAuth
+// @Tags admin
+// @Produce json
+// @Success 200 {object} handler.RespBody{data=schema.SiteThemeResp}
+// @Router /answer/admin/api/siteinfo/theme [get]
+func (sc *SiteInfoController) GetSiteTheme(ctx *gin.Context) {
+ resp, err := sc.siteInfoService.GetSiteTheme(ctx)
+ handler.HandleResponse(ctx, err, resp)
+}
+
+// GetRobots get site robots information
+// @Summary get site robots information
+// @Description get site robots information
+// @Tags site
+// @Produce json
+// @Success 200 {string} txt ""
+// @Router /robots.txt [get]
+func (sc *SiteInfoController) GetRobots(ctx *gin.Context) {
+ resp, err := sc.siteInfoService.GetSeo(ctx)
+ if err != nil {
+ ctx.String(http.StatusOK, "")
+ return
+ }
+ ctx.String(http.StatusOK, resp.Robots)
+}
+
+// GetRobots get site robots information
+// @Summary get site robots information
+// @Description get site robots information
+// @Tags site
+// @Produce json
+// @Success 200 {string} txt ""
+// @Router /custom.css [get]
+func (sc *SiteInfoController) GetCss(ctx *gin.Context) {
+ resp, err := sc.siteInfoService.GetSiteCustomCssHTML(ctx)
+ if err != nil {
+ ctx.String(http.StatusOK, "")
+ return
+ }
+ ctx.Header("content-type", "text/css;charset=utf-8")
+ ctx.String(http.StatusOK, resp.CustomCss)
+}
+
+// UpdateSeo update site seo information
+// @Summary update site seo information
+// @Description update site seo information
+// @Security ApiKeyAuth
+// @Tags admin
+// @Produce json
+// @Param data body schema.SiteSeoReq true "seo"
+// @Success 200 {object} handler.RespBody{}
+// @Router /answer/admin/api/siteinfo/seo [put]
+func (sc *SiteInfoController) UpdateSeo(ctx *gin.Context) {
+ req := schema.SiteSeoReq{}
+ if handler.BindAndCheck(ctx, &req) {
+ return
+ }
+ err := sc.siteInfoService.SaveSeo(ctx, req)
+ handler.HandleResponse(ctx, err, nil)
+}
+
// UpdateGeneral update site general information
// @Summary update site general information
// @Description update site general information
@@ -177,6 +282,60 @@ func (sc *SiteInfoController) UpdateSiteLegal(ctx *gin.Context) {
handler.HandleResponse(ctx, err, nil)
}
+// UpdateSiteLogin update site login
+// @Summary update site login
+// @Description update site login
+// @Security ApiKeyAuth
+// @Tags admin
+// @Produce json
+// @Param data body schema.SiteLoginReq true "login info"
+// @Success 200 {object} handler.RespBody{}
+// @Router /answer/admin/api/siteinfo/login [put]
+func (sc *SiteInfoController) UpdateSiteLogin(ctx *gin.Context) {
+ req := &schema.SiteLoginReq{}
+ if handler.BindAndCheck(ctx, req) {
+ return
+ }
+ err := sc.siteInfoService.SaveSiteLogin(ctx, req)
+ handler.HandleResponse(ctx, err, nil)
+}
+
+// UpdateSiteCustomCssHTML update site custom css html config
+// @Summary update site custom css html config
+// @Description update site custom css html config
+// @Security ApiKeyAuth
+// @Tags admin
+// @Produce json
+// @Param data body schema.SiteCustomCssHTMLReq true "login info"
+// @Success 200 {object} handler.RespBody{}
+// @Router /answer/admin/api/siteinfo/custom-css-html [put]
+func (sc *SiteInfoController) UpdateSiteCustomCssHTML(ctx *gin.Context) {
+ req := &schema.SiteCustomCssHTMLReq{}
+ if handler.BindAndCheck(ctx, req) {
+ return
+ }
+ err := sc.siteInfoService.SaveSiteCustomCssHTML(ctx, req)
+ handler.HandleResponse(ctx, err, nil)
+}
+
+// SaveSiteTheme update site custom css html config
+// @Summary update site custom css html config
+// @Description update site custom css html config
+// @Security ApiKeyAuth
+// @Tags admin
+// @Produce json
+// @Param data body schema.SiteThemeReq true "login info"
+// @Success 200 {object} handler.RespBody{}
+// @Router /answer/admin/api/siteinfo/theme [put]
+func (sc *SiteInfoController) SaveSiteTheme(ctx *gin.Context) {
+ req := &schema.SiteThemeReq{}
+ if handler.BindAndCheck(ctx, req) {
+ return
+ }
+ err := sc.siteInfoService.SaveSiteTheme(ctx, req)
+ handler.HandleResponse(ctx, err, nil)
+}
+
// GetSMTPConfig get smtp config
// @Summary GetSMTPConfig get smtp config
// @Description GetSMTPConfig get smtp config
diff --git a/internal/controller_backyard/user_backyard_controller.go b/internal/controller_backyard/user_backyard_controller.go
index 03d56d96..1b35a2ec 100644
--- a/internal/controller_backyard/user_backyard_controller.go
+++ b/internal/controller_backyard/user_backyard_controller.go
@@ -2,6 +2,7 @@ package controller_backyard
import (
"github.com/answerdev/answer/internal/base/handler"
+ "github.com/answerdev/answer/internal/base/middleware"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/user_backyard"
"github.com/gin-gonic/gin"
@@ -37,6 +38,72 @@ func (uc *UserBackyardController) UpdateUserStatus(ctx *gin.Context) {
handler.HandleResponse(ctx, err, nil)
}
+// UpdateUserRole update user role
+// @Summary update user role
+// @Description update user role
+// @Security ApiKeyAuth
+// @Tags admin
+// @Accept json
+// @Produce json
+// @Param data body schema.UpdateUserRoleReq true "user"
+// @Success 200 {object} handler.RespBody
+// @Router /answer/admin/api/user/role [put]
+func (uc *UserBackyardController) UpdateUserRole(ctx *gin.Context) {
+ req := &schema.UpdateUserRoleReq{}
+ if handler.BindAndCheck(ctx, req) {
+ return
+ }
+
+ req.LoginUserID = middleware.GetLoginUserIDFromContext(ctx)
+
+ err := uc.userService.UpdateUserRole(ctx, req)
+ handler.HandleResponse(ctx, err, nil)
+}
+
+// AddUser add user
+// @Summary add user
+// @Description add user
+// @Security ApiKeyAuth
+// @Tags admin
+// @Accept json
+// @Produce json
+// @Param data body schema.AddUserReq true "user"
+// @Success 200 {object} handler.RespBody
+// @Router /answer/admin/api/user [post]
+func (uc *UserBackyardController) AddUser(ctx *gin.Context) {
+ req := &schema.AddUserReq{}
+ if handler.BindAndCheck(ctx, req) {
+ return
+ }
+
+ req.LoginUserID = middleware.GetLoginUserIDFromContext(ctx)
+
+ err := uc.userService.AddUser(ctx, req)
+ handler.HandleResponse(ctx, err, nil)
+}
+
+// UpdateUserPassword update user password
+// @Summary update user password
+// @Description update user password
+// @Security ApiKeyAuth
+// @Tags admin
+// @Accept json
+// @Produce json
+// @Param data body schema.UpdateUserPasswordReq true "user"
+// @Success 200 {object} handler.RespBody
+// @Router /answer/admin/api/user/password [put]
+func (uc *UserBackyardController) UpdateUserPassword(ctx *gin.Context) {
+ req := &schema.UpdateUserPasswordReq{}
+ if handler.BindAndCheck(ctx, req) {
+ return
+ }
+
+ req.LoginUserID = middleware.GetLoginUserIDFromContext(ctx)
+
+ err := uc.userService.UpdateUserPassword(ctx, req)
+ handler.HandleResponse(ctx, err, nil)
+}
+
// GetUserPage get user page
// @Summary get user page
// @Description get user page
@@ -46,6 +113,7 @@ func (uc *UserBackyardController) UpdateUserStatus(ctx *gin.Context) {
// @Param page query int false "page size"
// @Param page_size query int false "page size"
// @Param query query string false "search query: email, username or id:[id]"
+// @Param staff query bool false "staff user"
// @Param status query string false "user status" Enums(suspended, deleted, inactive)
// @Success 200 {object} handler.RespBody{data=pager.PageModel{records=[]schema.GetUserPageResp}}
// @Router /answer/admin/api/users/page [get]
diff --git a/internal/entity/activity_entity.go b/internal/entity/activity_entity.go
index 4db4eadd..61c1619c 100644
--- a/internal/entity/activity_entity.go
+++ b/internal/entity/activity_entity.go
@@ -28,6 +28,16 @@ type ActivityRankSum struct {
Rank int `xorm:"not null default 0 INT(11) rank"`
}
+type ActivityUserRankStat struct {
+ UserID string `xorm:"user_id"`
+ Rank int `xorm:"rank_amount"`
+}
+
+type ActivityUserVoteStat struct {
+ UserID string `xorm:"user_id"`
+ VoteCount int `xorm:"vote_count"`
+}
+
// TableName activity table name
func (Activity) TableName() string {
return "activity"
diff --git a/internal/entity/power_entity.go b/internal/entity/power_entity.go
new file mode 100644
index 00000000..7efb2f28
--- /dev/null
+++ b/internal/entity/power_entity.go
@@ -0,0 +1,18 @@
+package entity
+
+import "time"
+
+// Power power
+type Power struct {
+ ID int `xorm:"not null pk autoincr INT(11) id"`
+ CreatedAt time.Time `xorm:"created TIMESTAMP created_at"`
+ UpdatedAt time.Time `xorm:"updated TIMESTAMP updated_at"`
+ Name string `xorm:"not null default '' VARCHAR(50) name"`
+ PowerType string `xorm:"not null default '' VARCHAR(100) power_type"`
+ Description string `xorm:"not null default '' VARCHAR(200) description"`
+}
+
+// TableName power table name
+func (Power) TableName() string {
+ return "power"
+}
diff --git a/internal/entity/role_entity.go b/internal/entity/role_entity.go
new file mode 100644
index 00000000..44b000c1
--- /dev/null
+++ b/internal/entity/role_entity.go
@@ -0,0 +1,17 @@
+package entity
+
+import "time"
+
+// Role role
+type Role struct {
+ ID int `xorm:"not null pk autoincr INT(11) id"`
+ CreatedAt time.Time `xorm:"created TIMESTAMP created_at"`
+ UpdatedAt time.Time `xorm:"updated TIMESTAMP updated_at"`
+ Name string `xorm:"not null default '' VARCHAR(50) name"`
+ Description string `xorm:"not null default '' VARCHAR(200) description"`
+}
+
+// TableName user table name
+func (Role) TableName() string {
+ return "role"
+}
diff --git a/internal/entity/role_power_rel_entity.go b/internal/entity/role_power_rel_entity.go
new file mode 100644
index 00000000..0c865cbe
--- /dev/null
+++ b/internal/entity/role_power_rel_entity.go
@@ -0,0 +1,17 @@
+package entity
+
+import "time"
+
+// RolePowerRel role power rel
+type RolePowerRel struct {
+ ID int `xorm:"not null pk autoincr INT(11) id"`
+ CreatedAt time.Time `xorm:"created TIMESTAMP created_at"`
+ UpdatedAt time.Time `xorm:"updated TIMESTAMP updated_at"`
+ RoleID int `xorm:"not null default 0 INT(11) role_id"`
+ PowerType string `xorm:"not null default '' VARCHAR(200) power_type"`
+}
+
+// TableName role power rel table name
+func (RolePowerRel) TableName() string {
+ return "role_power_rel"
+}
diff --git a/internal/entity/user_role_rel_entity.go b/internal/entity/user_role_rel_entity.go
new file mode 100644
index 00000000..bf1064df
--- /dev/null
+++ b/internal/entity/user_role_rel_entity.go
@@ -0,0 +1,17 @@
+package entity
+
+import "time"
+
+// UserRoleRel role
+type UserRoleRel struct {
+ ID int `xorm:"not null pk autoincr INT(11) id"`
+ CreatedAt time.Time `xorm:"created TIMESTAMP created_at"`
+ UpdatedAt time.Time `xorm:"updated TIMESTAMP updated_at"`
+ UserID string `xorm:"not null default 0 BIGINT(20) user_id"`
+ RoleID int `xorm:"not null default 0 INT(11) role_id"`
+}
+
+// TableName user role rel table name
+func (UserRoleRel) TableName() string {
+ return "user_role_rel"
+}
diff --git a/internal/install/install_controller.go b/internal/install/install_controller.go
index 62cbdf87..561c6dc4 100644
--- a/internal/install/install_controller.go
+++ b/internal/install/install_controller.go
@@ -187,5 +187,4 @@ func InitBaseInfo(ctx *gin.Context) {
time.Sleep(1 * time.Second)
os.Exit(0)
}()
- return
}
diff --git a/internal/migrations/init.go b/internal/migrations/init.go
index 9c4bb522..e1a6a829 100644
--- a/internal/migrations/init.go
+++ b/internal/migrations/init.go
@@ -6,10 +6,28 @@ import (
"github.com/answerdev/answer/internal/base/data"
"github.com/answerdev/answer/internal/entity"
+ "github.com/answerdev/answer/internal/service/permission"
"golang.org/x/crypto/bcrypt"
"xorm.io/xorm"
)
+const (
+ defaultSEORobotTxt = `User-agent: *
+Disallow: /admin
+Disallow: /search
+Disallow: /install
+Disallow: /review
+Disallow: /users/login
+Disallow: /users/register
+Disallow: /users/account-recovery
+Disallow: /users/oauth/*
+Disallow: /users/*/*
+Disallow: /answer/api
+Disallow: /*?code*
+
+Sitemap: `
+)
+
var tables = []interface{}{
&entity.Activity{},
&entity.Answer{},
@@ -28,6 +46,10 @@ var tables = []interface{}{
&entity.Uniqid{},
&entity.User{},
&entity.Version{},
+ &entity.Role{},
+ &entity.RolePowerRel{},
+ &entity.Power{},
+ &entity.UserRoleRel{},
}
// InitDB init db
@@ -51,6 +73,10 @@ func InitDB(dataConf *data.Database) (err error) {
if err != nil {
return fmt.Errorf("sync table failed: %s", err)
}
+ _, err = engine.InsertOne(&entity.Version{ID: 1, VersionNumber: ExpectedVersion()})
+ if err != nil {
+ return fmt.Errorf("init version table failed: %s", err)
+ }
err = initAdminUser(engine)
if err != nil {
@@ -61,11 +87,17 @@ func InitDB(dataConf *data.Database) (err error) {
if err != nil {
return fmt.Errorf("init config table: %s", err)
}
+
+ err = initRolePower(engine)
+ if err != nil {
+ return fmt.Errorf("init role and power failed: %s", err)
+ }
return nil
}
func initAdminUser(engine *xorm.Engine) error {
_, err := engine.InsertOne(&entity.User{
+ ID: "1",
Username: "admin",
Pass: "$2a$10$.gnUnpW.8ssRNaEvx.XwvOR2NuPsGzFLWWX2rqSIVAdIvLNZZYs5y", // admin
EMail: "admin@admin.com",
@@ -74,7 +106,6 @@ func initAdminUser(engine *xorm.Engine) error {
Status: 1,
Rank: 1,
DisplayName: "admin",
- IsAdmin: true,
})
return err
}
@@ -106,13 +137,43 @@ func initSiteInfo(engine *xorm.Engine, language, siteName, siteURL, contactEmail
Content: string(generalDataBytes),
Status: 1,
})
+ if err != nil {
+ return err
+ }
+
+ loginConfig := map[string]bool{
+ "allow_new_registrations": true,
+ "login_required": false,
+ }
+ loginConfigDataBytes, _ := json.Marshal(loginConfig)
+ _, err = engine.InsertOne(&entity.SiteInfo{
+ Type: "login",
+ Content: string(loginConfigDataBytes),
+ Status: 1,
+ })
+ if err != nil {
+ return err
+ }
+
+ seoData := map[string]string{
+ "robots": defaultSEORobotTxt + siteURL + "/sitemap.xml",
+ }
+ seoDataBytes, _ := json.Marshal(seoData)
+ _, err = engine.InsertOne(&entity.SiteInfo{
+ Type: "seo",
+ Content: string(seoDataBytes),
+ Status: 1,
+ })
+ if err != nil {
+ return err
+ }
return err
}
func updateAdminInfo(engine *xorm.Engine, adminName, adminPassword, adminEmail string) error {
generateFromPassword, err := bcrypt.GenerateFromPassword([]byte(adminPassword), bcrypt.DefaultCost)
if err != nil {
- return fmt.Errorf("")
+ return err
}
adminPassword = string(generateFromPassword)
@@ -191,26 +252,26 @@ func initConfigTable(engine *xorm.Engine) error {
{ID: 32, Key: "question.follow", Value: `0`},
{ID: 33, Key: "email.config", Value: `{"from_name":"","from_email":"","smtp_host":"","smtp_port":465,"smtp_password":"","smtp_username":"","smtp_authentication":true,"encryption":"","register_title":"[{{.SiteName}}] Confirm your new account","register_body":"Welcome to {{.SiteName}}
\n\nClick the following link to confirm and activate your new account:
\n{{.RegisterUrl}}
\n\nIf the above link is not clickable, try copying and pasting it into the address bar of your web browser.\n","pass_reset_title":"[{{.SiteName }}] Password reset","pass_reset_body":"Somebody asked to reset your password on [{{.SiteName}}].
\n\nIf it was not you, you can safely ignore this email.
\n\nClick the following link to choose a new password:
\n{{.PassResetUrl}}\n","change_title":"[{{.SiteName}}] Confirm your new email address","change_body":"Confirm your new email address for {{.SiteName}} by clicking on the following link:
\n\n{{.ChangeEmailUrl}}
\n\nIf you did not request this change, please ignore this email.\n","test_title":"[{{.SiteName}}] Test Email","test_body":"This is a test email."}`},
{ID: 35, Key: "tag.follow", Value: `0`},
- {ID: 36, Key: "rank.question.add", Value: `0`},
- {ID: 37, Key: "rank.question.edit", Value: `0`},
- {ID: 38, Key: "rank.question.delete", Value: `0`},
- {ID: 39, Key: "rank.question.vote_up", Value: `0`},
- {ID: 40, Key: "rank.question.vote_down", Value: `0`},
- {ID: 41, Key: "rank.answer.add", Value: `0`},
- {ID: 42, Key: "rank.answer.edit", Value: `0`},
- {ID: 43, Key: "rank.answer.delete", Value: `0`},
- {ID: 44, Key: "rank.answer.accept", Value: `0`},
- {ID: 45, Key: "rank.answer.vote_up", Value: `0`},
- {ID: 46, Key: "rank.answer.vote_down", Value: `0`},
- {ID: 47, Key: "rank.comment.add", Value: `0`},
- {ID: 48, Key: "rank.comment.edit", Value: `0`},
- {ID: 49, Key: "rank.comment.delete", Value: `0`},
- {ID: 50, Key: "rank.report.add", Value: `0`},
- {ID: 51, Key: "rank.tag.add", Value: `0`},
- {ID: 52, Key: "rank.tag.edit", Value: `0`},
- {ID: 53, Key: "rank.tag.delete", Value: `0`},
- {ID: 54, Key: "rank.tag.synonym", Value: `0`},
- {ID: 55, Key: "rank.link.url_limit", Value: `0`},
+ {ID: 36, Key: "rank.question.add", Value: `1`},
+ {ID: 37, Key: "rank.question.edit", Value: `200`},
+ {ID: 38, Key: "rank.question.delete", Value: `-1`},
+ {ID: 39, Key: "rank.question.vote_up", Value: `15`},
+ {ID: 40, Key: "rank.question.vote_down", Value: `125`},
+ {ID: 41, Key: "rank.answer.add", Value: `1`},
+ {ID: 42, Key: "rank.answer.edit", Value: `200`},
+ {ID: 43, Key: "rank.answer.delete", Value: `-1`},
+ {ID: 44, Key: "rank.answer.accept", Value: `1`},
+ {ID: 45, Key: "rank.answer.vote_up", Value: `15`},
+ {ID: 46, Key: "rank.answer.vote_down", Value: `125`},
+ {ID: 47, Key: "rank.comment.add", Value: `1`},
+ {ID: 48, Key: "rank.comment.edit", Value: `-1`},
+ {ID: 49, Key: "rank.comment.delete", Value: `-1`},
+ {ID: 50, Key: "rank.report.add", Value: `1`},
+ {ID: 51, Key: "rank.tag.add", Value: `1`},
+ {ID: 52, Key: "rank.tag.edit", Value: `100`},
+ {ID: 53, Key: "rank.tag.delete", Value: `-1`},
+ {ID: 54, Key: "rank.tag.synonym", Value: `20000`},
+ {ID: 55, Key: "rank.link.url_limit", Value: `10`},
{ID: 56, Key: "rank.vote.detail", Value: `0`},
{ID: 57, Key: "reason.spam", Value: `{"name":"spam","description":"This post is an advertisement, or vandalism. It is not useful or relevant to the current topic."}`},
{ID: 58, Key: "reason.rude_or_abusive", Value: `{"name":"rude or abusive","description":"A reasonable person would find this content inappropriate for respectful discourse."}`},
@@ -262,7 +323,155 @@ func initConfigTable(engine *xorm.Engine) error {
{ID: 104, Key: "tag.rollback", Value: `0`},
{ID: 105, Key: "tag.deleted", Value: `0`},
{ID: 106, Key: "tag.undeleted", Value: `0`},
+ {ID: 107, Key: "rank.comment.vote_up", Value: `1`},
+ {ID: 108, Key: "rank.comment.vote_down", Value: `1`},
+ {ID: 109, Key: "rank.question.edit_without_review", Value: `2000`},
+ {ID: 110, Key: "rank.answer.edit_without_review", Value: `2000`},
+ {ID: 111, Key: "rank.tag.edit_without_review", Value: `20000`},
+ {ID: 112, Key: "rank.answer.audit", Value: `2000`},
+ {ID: 113, Key: "rank.question.audit", Value: `2000`},
+ {ID: 114, Key: "rank.tag.audit", Value: `20000`},
+ {ID: 115, Key: "rank.question.close", Value: `-1`},
+ {ID: 116, Key: "rank.question.reopen", Value: `-1`},
+ {ID: 117, Key: "rank.tag.use_reserved_tag", Value: `-1`},
}
_, err := engine.Insert(defaultConfigTable)
return err
}
+
+func initRolePower(engine *xorm.Engine) (err error) {
+ roles := []*entity.Role{
+ {ID: 1, Name: "User", Description: "Default with no special access."},
+ {ID: 2, Name: "Admin", Description: "Have the full power to access the site."},
+ {ID: 3, Name: "Moderator", Description: "Has access to all posts except admin settings."},
+ }
+ _, err = engine.Insert(roles)
+ if err != nil {
+ return err
+ }
+
+ powers := []*entity.Power{
+ {ID: 1, Name: "admin access", PowerType: permission.AdminAccess, Description: "admin access"},
+ {ID: 2, Name: "question add", PowerType: permission.QuestionAdd, Description: "question add"},
+ {ID: 3, Name: "question edit", PowerType: permission.QuestionEdit, Description: "question edit"},
+ {ID: 4, Name: "question edit without review", PowerType: permission.QuestionEditWithoutReview, Description: "question edit without review"},
+ {ID: 5, Name: "question delete", PowerType: permission.QuestionDelete, Description: "question delete"},
+ {ID: 6, Name: "question close", PowerType: permission.QuestionClose, Description: "question close"},
+ {ID: 7, Name: "question reopen", PowerType: permission.QuestionReopen, Description: "question reopen"},
+ {ID: 8, Name: "question vote up", PowerType: permission.QuestionVoteUp, Description: "question vote up"},
+ {ID: 9, Name: "question vote down", PowerType: permission.QuestionVoteDown, Description: "question vote down"},
+ {ID: 10, Name: "answer add", PowerType: permission.AnswerAdd, Description: "answer add"},
+ {ID: 11, Name: "answer edit", PowerType: permission.AnswerEdit, Description: "answer edit"},
+ {ID: 12, Name: "answer edit without review", PowerType: permission.AnswerEditWithoutReview, Description: "answer edit without review"},
+ {ID: 13, Name: "answer delete", PowerType: permission.AnswerDelete, Description: "answer delete"},
+ {ID: 14, Name: "answer accept", PowerType: permission.AnswerAccept, Description: "answer accept"},
+ {ID: 15, Name: "answer vote up", PowerType: permission.AnswerVoteUp, Description: "answer vote up"},
+ {ID: 16, Name: "answer vote down", PowerType: permission.AnswerVoteDown, Description: "answer vote down"},
+ {ID: 17, Name: "comment add", PowerType: permission.CommentAdd, Description: "comment add"},
+ {ID: 18, Name: "comment edit", PowerType: permission.CommentEdit, Description: "comment edit"},
+ {ID: 19, Name: "comment delete", PowerType: permission.CommentDelete, Description: "comment delete"},
+ {ID: 20, Name: "comment vote up", PowerType: permission.CommentVoteUp, Description: "comment vote up"},
+ {ID: 21, Name: "comment vote down", PowerType: permission.CommentVoteDown, Description: "comment vote down"},
+ {ID: 22, Name: "report add", PowerType: permission.ReportAdd, Description: "report add"},
+ {ID: 23, Name: "tag add", PowerType: permission.TagAdd, Description: "tag add"},
+ {ID: 24, Name: "tag edit", PowerType: permission.TagEdit, Description: "tag edit"},
+ {ID: 25, Name: "tag edit without review", PowerType: permission.TagEditWithoutReview, Description: "tag edit without review"},
+ {ID: 26, Name: "tag edit slug name", PowerType: permission.TagEditSlugName, Description: "tag edit slug name"},
+ {ID: 27, Name: "tag delete", PowerType: permission.TagDelete, Description: "tag delete"},
+ {ID: 28, Name: "tag synonym", PowerType: permission.TagSynonym, Description: "tag synonym"},
+ {ID: 29, Name: "link url limit", PowerType: permission.LinkUrlLimit, Description: "link url limit"},
+ {ID: 30, Name: "vote detail", PowerType: permission.VoteDetail, Description: "vote detail"},
+ {ID: 31, Name: "answer audit", PowerType: permission.AnswerAudit, Description: "answer audit"},
+ {ID: 32, Name: "question audit", PowerType: permission.QuestionAudit, Description: "question audit"},
+ {ID: 33, Name: "tag audit", PowerType: permission.TagAudit, Description: "tag audit"},
+ }
+ _, err = engine.Insert(powers)
+ if err != nil {
+ return err
+ }
+
+ rolePowerRels := []*entity.RolePowerRel{
+ {RoleID: 2, PowerType: permission.AdminAccess},
+ {RoleID: 2, PowerType: permission.QuestionAdd},
+ {RoleID: 2, PowerType: permission.QuestionEdit},
+ {RoleID: 2, PowerType: permission.QuestionEditWithoutReview},
+ {RoleID: 2, PowerType: permission.QuestionDelete},
+ {RoleID: 2, PowerType: permission.QuestionClose},
+ {RoleID: 2, PowerType: permission.QuestionReopen},
+ {RoleID: 2, PowerType: permission.QuestionVoteUp},
+ {RoleID: 2, PowerType: permission.QuestionVoteDown},
+ {RoleID: 2, PowerType: permission.AnswerAdd},
+ {RoleID: 2, PowerType: permission.AnswerEdit},
+ {RoleID: 2, PowerType: permission.AnswerEditWithoutReview},
+ {RoleID: 2, PowerType: permission.AnswerDelete},
+ {RoleID: 2, PowerType: permission.AnswerAccept},
+ {RoleID: 2, PowerType: permission.AnswerVoteUp},
+ {RoleID: 2, PowerType: permission.AnswerVoteDown},
+ {RoleID: 2, PowerType: permission.CommentAdd},
+ {RoleID: 2, PowerType: permission.CommentEdit},
+ {RoleID: 2, PowerType: permission.CommentDelete},
+ {RoleID: 2, PowerType: permission.CommentVoteUp},
+ {RoleID: 2, PowerType: permission.CommentVoteDown},
+ {RoleID: 2, PowerType: permission.ReportAdd},
+ {RoleID: 2, PowerType: permission.TagAdd},
+ {RoleID: 2, PowerType: permission.TagEdit},
+ {RoleID: 2, PowerType: permission.TagEditSlugName},
+ {RoleID: 2, PowerType: permission.TagEditWithoutReview},
+ {RoleID: 2, PowerType: permission.TagDelete},
+ {RoleID: 2, PowerType: permission.TagSynonym},
+ {RoleID: 2, PowerType: permission.LinkUrlLimit},
+ {RoleID: 2, PowerType: permission.VoteDetail},
+ {RoleID: 2, PowerType: permission.AnswerAudit},
+ {RoleID: 2, PowerType: permission.QuestionAudit},
+ {RoleID: 2, PowerType: permission.TagAudit},
+ {RoleID: 2, PowerType: permission.TagUseReservedTag},
+
+ {RoleID: 3, PowerType: permission.QuestionAdd},
+ {RoleID: 3, PowerType: permission.QuestionEdit},
+ {RoleID: 3, PowerType: permission.QuestionEditWithoutReview},
+ {RoleID: 3, PowerType: permission.QuestionDelete},
+ {RoleID: 3, PowerType: permission.QuestionClose},
+ {RoleID: 3, PowerType: permission.QuestionReopen},
+ {RoleID: 3, PowerType: permission.QuestionVoteUp},
+ {RoleID: 3, PowerType: permission.QuestionVoteDown},
+ {RoleID: 3, PowerType: permission.AnswerAdd},
+ {RoleID: 3, PowerType: permission.AnswerEdit},
+ {RoleID: 3, PowerType: permission.AnswerEditWithoutReview},
+ {RoleID: 3, PowerType: permission.AnswerDelete},
+ {RoleID: 3, PowerType: permission.AnswerAccept},
+ {RoleID: 3, PowerType: permission.AnswerVoteUp},
+ {RoleID: 3, PowerType: permission.AnswerVoteDown},
+ {RoleID: 3, PowerType: permission.CommentAdd},
+ {RoleID: 3, PowerType: permission.CommentEdit},
+ {RoleID: 3, PowerType: permission.CommentDelete},
+ {RoleID: 3, PowerType: permission.CommentVoteUp},
+ {RoleID: 3, PowerType: permission.CommentVoteDown},
+ {RoleID: 3, PowerType: permission.ReportAdd},
+ {RoleID: 3, PowerType: permission.TagAdd},
+ {RoleID: 3, PowerType: permission.TagEdit},
+ {RoleID: 3, PowerType: permission.TagEditSlugName},
+ {RoleID: 3, PowerType: permission.TagEditWithoutReview},
+ {RoleID: 3, PowerType: permission.TagDelete},
+ {RoleID: 3, PowerType: permission.TagSynonym},
+ {RoleID: 3, PowerType: permission.LinkUrlLimit},
+ {RoleID: 3, PowerType: permission.VoteDetail},
+ {RoleID: 3, PowerType: permission.AnswerAudit},
+ {RoleID: 3, PowerType: permission.QuestionAudit},
+ {RoleID: 3, PowerType: permission.TagAudit},
+ {RoleID: 3, PowerType: permission.TagUseReservedTag},
+ }
+ _, err = engine.Insert(rolePowerRels)
+ if err != nil {
+ return err
+ }
+
+ adminUserRoleRel := &entity.UserRoleRel{
+ UserID: "1",
+ RoleID: 2,
+ }
+ _, err = engine.Insert(adminUserRoleRel)
+ if err != nil {
+ return err
+ }
+ return nil
+}
diff --git a/internal/migrations/migrations.go b/internal/migrations/migrations.go
index 0e74d1d4..44aaec56 100644
--- a/internal/migrations/migrations.go
+++ b/internal/migrations/migrations.go
@@ -45,6 +45,8 @@ var migrations = []Migration{
NewMigration("add user language", addUserLanguage),
NewMigration("add recommend and reserved tag fields", addTagRecommendedAndReserved),
NewMigration("add activity timeline", addActivityTimeline),
+ NewMigration("add user role", addRoleFeatures),
+ NewMigration("add theme and private mode", addThemeAndPrivateMode),
}
// GetCurrentDBVersion returns the current db version
diff --git a/internal/migrations/v1.go b/internal/migrations/v1.go
index b9e316d9..e929d45c 100644
--- a/internal/migrations/v1.go
+++ b/internal/migrations/v1.go
@@ -6,6 +6,8 @@ import (
func addUserLanguage(x *xorm.Engine) error {
type User struct {
+ ID string `xorm:"not null pk autoincr BIGINT(20) id"`
+ Username string `xorm:"not null default '' VARCHAR(50) UNIQUE username"`
Language string `xorm:"not null default '' VARCHAR(100) language"`
}
return x.Sync(new(User))
diff --git a/internal/migrations/v2.go b/internal/migrations/v2.go
index b006daf8..368124ef 100644
--- a/internal/migrations/v2.go
+++ b/internal/migrations/v2.go
@@ -6,8 +6,10 @@ import (
func addTagRecommendedAndReserved(x *xorm.Engine) error {
type Tag struct {
- Recommend bool `xorm:"not null default false BOOL recommend"`
- Reserved bool `xorm:"not null default false BOOL reserved"`
+ ID string `xorm:"not null pk comment('tag_id') BIGINT(20) id"`
+ SlugName string `xorm:"not null default '' unique VARCHAR(35) slug_name"`
+ Recommend bool `xorm:"not null default false BOOL recommend"`
+ Reserved bool `xorm:"not null default false BOOL reserved"`
}
return x.Sync(new(Tag))
}
diff --git a/internal/migrations/v3.go b/internal/migrations/v3.go
index e1f95bb4..076a72b5 100644
--- a/internal/migrations/v3.go
+++ b/internal/migrations/v3.go
@@ -95,6 +95,7 @@ ON "question" (
// only increasing field length to 128
type Config struct {
+ ID int `xorm:"not null pk autoincr INT(11) id"`
Key string `xorm:"unique VARCHAR(128) key"`
}
if err := x.Sync(new(Config)); err != nil {
@@ -172,22 +173,34 @@ ON "question" (
}
type Revision struct {
- ReviewUserID int64 `xorm:"not null default 0 BIGINT(20) review_user_id"`
+ ID string `xorm:"not null pk autoincr BIGINT(20) id"`
+ ObjectID string `xorm:"not null default 0 BIGINT(20) INDEX object_id"`
+ ReviewUserID int64 `xorm:"not null default 0 BIGINT(20) review_user_id"`
}
type Activity struct {
+ ID string `xorm:"not null pk autoincr BIGINT(20) id"`
CancelledAt time.Time `xorm:"TIMESTAMP cancelled_at"`
+ UserID string `xorm:"not null index BIGINT(20) user_id"`
+ TriggerUserID int64 `xorm:"not null default 0 index BIGINT(20) trigger_user_id"`
+ ObjectID string `xorm:"not null default 0 index BIGINT(20) object_id"`
RevisionID int64 `xorm:"not null default 0 BIGINT(20) revision_id"`
OriginalObjectID string `xorm:"not null default 0 BIGINT(20) original_object_id"`
}
type Tag struct {
- UserID string `xorm:"not null default 0 BIGINT(20) user_id"`
+ ID string `xorm:"not null pk comment('tag_id') BIGINT(20) id"`
+ SlugName string `xorm:"not null default '' unique VARCHAR(35) slug_name"`
+ UserID string `xorm:"not null default 0 BIGINT(20) user_id"`
}
type Question struct {
+ ID string `xorm:"not null pk BIGINT(20) id"`
+ UserID string `xorm:"not null default 0 BIGINT(20) INDEX user_id"`
UpdatedAt time.Time `xorm:"updated_at TIMESTAMP"`
LastEditUserID string `xorm:"not null default 0 BIGINT(20) last_edit_user_id"`
PostUpdateTime time.Time `xorm:"post_update_time TIMESTAMP"`
}
type Answer struct {
+ ID string `xorm:"not null pk autoincr BIGINT(20) id"`
+ UserID string `xorm:"not null default 0 BIGINT(20) INDEX user_id"`
UpdatedAt time.Time `xorm:"updated_at TIMESTAMP"`
LastEditUserID string `xorm:"not null default 0 BIGINT(20) last_edit_user_id"`
}
diff --git a/internal/migrations/v4.go b/internal/migrations/v4.go
new file mode 100644
index 00000000..9d32b13f
--- /dev/null
+++ b/internal/migrations/v4.go
@@ -0,0 +1,215 @@
+package migrations
+
+import (
+ "fmt"
+
+ "github.com/answerdev/answer/internal/entity"
+ "github.com/answerdev/answer/internal/service/permission"
+ "github.com/segmentfault/pacman/log"
+ "xorm.io/xorm"
+)
+
+func addRoleFeatures(x *xorm.Engine) error {
+ err := x.Sync(new(entity.Role), new(entity.RolePowerRel), new(entity.Power), new(entity.UserRoleRel))
+ if err != nil {
+ return err
+ }
+
+ roles := []*entity.Role{
+ {ID: 1, Name: "User", Description: "Default with no special access."},
+ {ID: 2, Name: "Admin", Description: "Have the full power to access the site."},
+ {ID: 3, Name: "Moderator", Description: "Has access to all posts except admin settings."},
+ }
+
+ // insert default roles
+ for _, role := range roles {
+ exist, err := x.Get(&entity.Role{ID: role.ID, Name: role.Name})
+ if err != nil {
+ return err
+ }
+ if exist {
+ continue
+ }
+ _, err = x.Insert(role)
+ if err != nil {
+ return err
+ }
+ }
+
+ powers := []*entity.Power{
+ {ID: 1, Name: "admin access", PowerType: permission.AdminAccess, Description: "admin access"},
+ {ID: 2, Name: "question add", PowerType: permission.QuestionAdd, Description: "question add"},
+ {ID: 3, Name: "question edit", PowerType: permission.QuestionEdit, Description: "question edit"},
+ {ID: 4, Name: "question edit without review", PowerType: permission.QuestionEditWithoutReview, Description: "question edit without review"},
+ {ID: 5, Name: "question delete", PowerType: permission.QuestionDelete, Description: "question delete"},
+ {ID: 6, Name: "question close", PowerType: permission.QuestionClose, Description: "question close"},
+ {ID: 7, Name: "question reopen", PowerType: permission.QuestionReopen, Description: "question reopen"},
+ {ID: 8, Name: "question vote up", PowerType: permission.QuestionVoteUp, Description: "question vote up"},
+ {ID: 9, Name: "question vote down", PowerType: permission.QuestionVoteDown, Description: "question vote down"},
+ {ID: 10, Name: "answer add", PowerType: permission.AnswerAdd, Description: "answer add"},
+ {ID: 11, Name: "answer edit", PowerType: permission.AnswerEdit, Description: "answer edit"},
+ {ID: 12, Name: "answer edit without review", PowerType: permission.AnswerEditWithoutReview, Description: "answer edit without review"},
+ {ID: 13, Name: "answer delete", PowerType: permission.AnswerDelete, Description: "answer delete"},
+ {ID: 14, Name: "answer accept", PowerType: permission.AnswerAccept, Description: "answer accept"},
+ {ID: 15, Name: "answer vote up", PowerType: permission.AnswerVoteUp, Description: "answer vote up"},
+ {ID: 16, Name: "answer vote down", PowerType: permission.AnswerVoteDown, Description: "answer vote down"},
+ {ID: 17, Name: "comment add", PowerType: permission.CommentAdd, Description: "comment add"},
+ {ID: 18, Name: "comment edit", PowerType: permission.CommentEdit, Description: "comment edit"},
+ {ID: 19, Name: "comment delete", PowerType: permission.CommentDelete, Description: "comment delete"},
+ {ID: 20, Name: "comment vote up", PowerType: permission.CommentVoteUp, Description: "comment vote up"},
+ {ID: 21, Name: "comment vote down", PowerType: permission.CommentVoteDown, Description: "comment vote down"},
+ {ID: 22, Name: "report add", PowerType: permission.ReportAdd, Description: "report add"},
+ {ID: 23, Name: "tag add", PowerType: permission.TagAdd, Description: "tag add"},
+ {ID: 24, Name: "tag edit", PowerType: permission.TagEdit, Description: "tag edit"},
+ {ID: 25, Name: "tag edit without review", PowerType: permission.TagEditWithoutReview, Description: "tag edit without review"},
+ {ID: 26, Name: "tag edit slug name", PowerType: permission.TagEditSlugName, Description: "tag edit slug name"},
+ {ID: 27, Name: "tag delete", PowerType: permission.TagDelete, Description: "tag delete"},
+ {ID: 28, Name: "tag synonym", PowerType: permission.TagSynonym, Description: "tag synonym"},
+ {ID: 29, Name: "link url limit", PowerType: permission.LinkUrlLimit, Description: "link url limit"},
+ {ID: 30, Name: "vote detail", PowerType: permission.VoteDetail, Description: "vote detail"},
+ {ID: 31, Name: "answer audit", PowerType: permission.AnswerAudit, Description: "answer audit"},
+ {ID: 32, Name: "question audit", PowerType: permission.QuestionAudit, Description: "question audit"},
+ {ID: 33, Name: "tag audit", PowerType: permission.TagAudit, Description: "tag audit"},
+ }
+ // insert default powers
+ for _, power := range powers {
+ exist, err := x.Get(&entity.Power{ID: power.ID})
+ if err != nil {
+ return err
+ }
+ if exist {
+ _, err = x.ID(power.ID).Update(power)
+ } else {
+ _, err = x.Insert(power)
+ }
+ if err != nil {
+ return err
+ }
+ }
+
+ rolePowerRels := []*entity.RolePowerRel{
+ {RoleID: 2, PowerType: permission.AdminAccess},
+ {RoleID: 2, PowerType: permission.QuestionAdd},
+ {RoleID: 2, PowerType: permission.QuestionEdit},
+ {RoleID: 2, PowerType: permission.QuestionEditWithoutReview},
+ {RoleID: 2, PowerType: permission.QuestionDelete},
+ {RoleID: 2, PowerType: permission.QuestionClose},
+ {RoleID: 2, PowerType: permission.QuestionReopen},
+ {RoleID: 2, PowerType: permission.QuestionVoteUp},
+ {RoleID: 2, PowerType: permission.QuestionVoteDown},
+ {RoleID: 2, PowerType: permission.AnswerAdd},
+ {RoleID: 2, PowerType: permission.AnswerEdit},
+ {RoleID: 2, PowerType: permission.AnswerEditWithoutReview},
+ {RoleID: 2, PowerType: permission.AnswerDelete},
+ {RoleID: 2, PowerType: permission.AnswerAccept},
+ {RoleID: 2, PowerType: permission.AnswerVoteUp},
+ {RoleID: 2, PowerType: permission.AnswerVoteDown},
+ {RoleID: 2, PowerType: permission.CommentAdd},
+ {RoleID: 2, PowerType: permission.CommentEdit},
+ {RoleID: 2, PowerType: permission.CommentDelete},
+ {RoleID: 2, PowerType: permission.CommentVoteUp},
+ {RoleID: 2, PowerType: permission.CommentVoteDown},
+ {RoleID: 2, PowerType: permission.ReportAdd},
+ {RoleID: 2, PowerType: permission.TagAdd},
+ {RoleID: 2, PowerType: permission.TagEdit},
+ {RoleID: 2, PowerType: permission.TagEditSlugName},
+ {RoleID: 2, PowerType: permission.TagEditWithoutReview},
+ {RoleID: 2, PowerType: permission.TagDelete},
+ {RoleID: 2, PowerType: permission.TagSynonym},
+ {RoleID: 2, PowerType: permission.LinkUrlLimit},
+ {RoleID: 2, PowerType: permission.VoteDetail},
+ {RoleID: 2, PowerType: permission.AnswerAudit},
+ {RoleID: 2, PowerType: permission.QuestionAudit},
+ {RoleID: 2, PowerType: permission.TagAudit},
+ {RoleID: 2, PowerType: permission.TagUseReservedTag},
+
+ {RoleID: 3, PowerType: permission.QuestionAdd},
+ {RoleID: 3, PowerType: permission.QuestionEdit},
+ {RoleID: 3, PowerType: permission.QuestionEditWithoutReview},
+ {RoleID: 3, PowerType: permission.QuestionDelete},
+ {RoleID: 3, PowerType: permission.QuestionClose},
+ {RoleID: 3, PowerType: permission.QuestionReopen},
+ {RoleID: 3, PowerType: permission.QuestionVoteUp},
+ {RoleID: 3, PowerType: permission.QuestionVoteDown},
+ {RoleID: 3, PowerType: permission.AnswerAdd},
+ {RoleID: 3, PowerType: permission.AnswerEdit},
+ {RoleID: 3, PowerType: permission.AnswerEditWithoutReview},
+ {RoleID: 3, PowerType: permission.AnswerDelete},
+ {RoleID: 3, PowerType: permission.AnswerAccept},
+ {RoleID: 3, PowerType: permission.AnswerVoteUp},
+ {RoleID: 3, PowerType: permission.AnswerVoteDown},
+ {RoleID: 3, PowerType: permission.CommentAdd},
+ {RoleID: 3, PowerType: permission.CommentEdit},
+ {RoleID: 3, PowerType: permission.CommentDelete},
+ {RoleID: 3, PowerType: permission.CommentVoteUp},
+ {RoleID: 3, PowerType: permission.CommentVoteDown},
+ {RoleID: 3, PowerType: permission.ReportAdd},
+ {RoleID: 3, PowerType: permission.TagAdd},
+ {RoleID: 3, PowerType: permission.TagEdit},
+ {RoleID: 3, PowerType: permission.TagEditSlugName},
+ {RoleID: 3, PowerType: permission.TagEditWithoutReview},
+ {RoleID: 3, PowerType: permission.TagDelete},
+ {RoleID: 3, PowerType: permission.TagSynonym},
+ {RoleID: 3, PowerType: permission.LinkUrlLimit},
+ {RoleID: 3, PowerType: permission.VoteDetail},
+ {RoleID: 3, PowerType: permission.AnswerAudit},
+ {RoleID: 3, PowerType: permission.QuestionAudit},
+ {RoleID: 3, PowerType: permission.TagAudit},
+ {RoleID: 3, PowerType: permission.TagUseReservedTag},
+ }
+
+ // insert default powers
+ for _, rel := range rolePowerRels {
+ exist, err := x.Get(&entity.RolePowerRel{RoleID: rel.RoleID, PowerType: rel.PowerType})
+ if err != nil {
+ return err
+ }
+ if exist {
+ continue
+ }
+ _, err = x.Insert(rel)
+ if err != nil {
+ return err
+ }
+ }
+
+ adminUserRoleRel := &entity.UserRoleRel{
+ UserID: "1",
+ RoleID: 2,
+ }
+
+ exist, err := x.Get(adminUserRoleRel)
+ if err != nil {
+ return err
+ }
+ if !exist {
+ _, err = x.Insert(adminUserRoleRel)
+ if err != nil {
+ return err
+ }
+ }
+
+ defaultConfigTable := []*entity.Config{
+ {ID: 115, Key: "rank.question.close", Value: `-1`},
+ {ID: 116, Key: "rank.question.reopen", Value: `-1`},
+ {ID: 117, Key: "rank.tag.use_reserved_tag", Value: `-1`},
+ }
+ for _, c := range defaultConfigTable {
+ exist, err := x.Get(&entity.Config{ID: c.ID, Key: c.Key})
+ if err != nil {
+ return fmt.Errorf("get config failed: %w", err)
+ }
+ if exist {
+ if _, err = x.Update(c, &entity.Config{ID: c.ID, Key: c.Key}); err != nil {
+ log.Errorf("update %+v config failed: %s", c, err)
+ return fmt.Errorf("update config failed: %w", err)
+ }
+ continue
+ }
+ if _, err = x.Insert(&entity.Config{ID: c.ID, Key: c.Key, Value: c.Value}); err != nil {
+ log.Errorf("insert %+v config failed: %s", c, err)
+ return fmt.Errorf("add config failed: %w", err)
+ }
+ }
+ return nil
+}
diff --git a/internal/migrations/v5.go b/internal/migrations/v5.go
new file mode 100644
index 00000000..fc4b057a
--- /dev/null
+++ b/internal/migrations/v5.go
@@ -0,0 +1,30 @@
+package migrations
+
+import (
+ "encoding/json"
+ "fmt"
+
+ "github.com/answerdev/answer/internal/entity"
+ "xorm.io/xorm"
+)
+
+func addThemeAndPrivateMode(x *xorm.Engine) error {
+ loginConfig := map[string]bool{
+ "allow_new_registrations": true,
+ "login_required": false,
+ }
+ loginConfigDataBytes, _ := json.Marshal(loginConfig)
+ siteInfo := &entity.SiteInfo{
+ Type: "login",
+ Content: string(loginConfigDataBytes),
+ Status: 1,
+ }
+ exist, err := x.Get(&entity.SiteInfo{Type: siteInfo.Type})
+ if err != nil {
+ return fmt.Errorf("get config failed: %w", err)
+ }
+ if !exist {
+ _, err = x.InsertOne(siteInfo)
+ }
+ return err
+}
diff --git a/internal/repo/activity/answer_repo.go b/internal/repo/activity/answer_repo.go
index 74d9136d..a6eb47cc 100644
--- a/internal/repo/activity/answer_repo.go
+++ b/internal/repo/activity/answer_repo.go
@@ -27,15 +27,12 @@ type AnswerActivityRepo struct {
}
const (
- acceptAction = "accept"
- acceptedAction = "accepted"
- acceptCancelAction = "accept_cancel"
- acceptedCancelAction = "accepted_cancel"
+ acceptAction = "accept"
+ acceptedAction = "accepted"
)
var (
- acceptActionList = []string{acceptAction, acceptedAction}
- acceptCancelActionList = []string{acceptCancelAction, acceptedCancelAction}
+ acceptActionList = []string{acceptAction, acceptedAction}
)
// NewAnswerActivityRepo new repository
diff --git a/internal/repo/activity/vote_repo.go b/internal/repo/activity/vote_repo.go
index f934329f..208028df 100644
--- a/internal/repo/activity/vote_repo.go
+++ b/internal/repo/activity/vote_repo.go
@@ -70,6 +70,7 @@ var LimitDownActions = map[string][]string{
func (vr *VoteRepo) vote(ctx context.Context, objectID string, userID, objectUserID string, actions []string) (resp *schema.VoteResp, err error) {
resp = &schema.VoteResp{}
+ notificationUserIDs := make([]string, 0)
_, err = vr.data.DB.Transaction(func(session *xorm.Session) (result any, err error) {
result = nil
for _, action := range actions {
@@ -126,8 +127,7 @@ func (vr *VoteRepo) vote(ctx context.Context, objectID string, userID, objectUse
if isReachStandard {
insertActivity.Rank = 0
}
-
- vr.sendNotification(ctx, activityUserID, objectUserID, objectID)
+ notificationUserIDs = append(notificationUserIDs, activityUserID)
}
if has {
@@ -165,11 +165,15 @@ func (vr *VoteRepo) vote(ctx context.Context, objectID string, userID, objectUse
resp, err = vr.GetVoteResultByObjectId(ctx, objectID)
resp.VoteStatus = vr.voteCommon.GetVoteStatus(ctx, objectID, userID)
+ for _, activityUserID := range notificationUserIDs {
+ vr.sendNotification(ctx, activityUserID, objectUserID, objectID)
+ }
return
}
func (vr *VoteRepo) voteCancel(ctx context.Context, objectID string, userID, objectUserID string, actions []string) (resp *schema.VoteResp, err error) {
resp = &schema.VoteResp{}
+ notificationUserIDs := make([]string, 0)
_, err = vr.data.DB.Transaction(func(session *xorm.Session) (result any, err error) {
for _, action := range actions {
var (
@@ -216,13 +220,12 @@ func (vr *VoteRepo) voteCancel(ctx context.Context, objectID string, userID, obj
}
// trigger user rank and send notification
- if hasRank != 0 {
+ if hasRank != 0 && existsActivity.Rank > 0 {
_, err = vr.userRankRepo.TriggerUserRank(ctx, session, activityUserID, -deltaRank, activityType)
if err != nil {
return
}
-
- vr.sendNotification(ctx, activityUserID, objectUserID, objectID)
+ notificationUserIDs = append(notificationUserIDs, activityUserID)
}
// update votes
@@ -245,6 +248,10 @@ func (vr *VoteRepo) voteCancel(ctx context.Context, objectID string, userID, obj
}
resp, err = vr.GetVoteResultByObjectId(ctx, objectID)
resp.VoteStatus = vr.voteCommon.GetVoteStatus(ctx, objectID, userID)
+
+ for _, activityUserID := range notificationUserIDs {
+ vr.sendNotification(ctx, activityUserID, objectUserID, objectID)
+ }
return
}
diff --git a/internal/repo/activity_common/activity_repo.go b/internal/repo/activity_common/activity_repo.go
index efb7d3be..5dede5fe 100644
--- a/internal/repo/activity_common/activity_repo.go
+++ b/internal/repo/activity_common/activity_repo.go
@@ -3,9 +3,11 @@ package activity_common
import (
"context"
"fmt"
+ "time"
"github.com/answerdev/answer/internal/entity"
"github.com/answerdev/answer/internal/service/activity_common"
+ "github.com/answerdev/answer/internal/service/activity_type"
"github.com/answerdev/answer/pkg/obj"
"xorm.io/builder"
"xorm.io/xorm"
@@ -106,3 +108,49 @@ func (ar *ActivityRepo) AddActivity(ctx context.Context, activity *entity.Activi
}
return
}
+
+// GetUsersWhoHasGainedTheMostReputation get users who has gained the most reputation over a period of time
+func (ar *ActivityRepo) GetUsersWhoHasGainedTheMostReputation(
+ ctx context.Context, startTime, endTime time.Time, limit int) (rankStat []*entity.ActivityUserRankStat, err error) {
+ rankStat = make([]*entity.ActivityUserRankStat, 0)
+ session := ar.data.DB.Select("user_id, SUM(rank) AS rank_amount").Table("activity")
+ session.Where("has_rank = 1 AND cancelled = 0")
+ session.Where("created_at >= ?", startTime)
+ session.Where("created_at <= ?", endTime)
+ session.GroupBy("user_id")
+ session.Desc("rank_amount")
+ session.Limit(limit)
+ err = session.Find(&rankStat)
+ if err != nil {
+ err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
+ }
+ return
+}
+
+// GetUsersWhoHasVoteMost get users who has vote most
+func (ar *ActivityRepo) GetUsersWhoHasVoteMost(
+ ctx context.Context, startTime, endTime time.Time, limit int) (voteStat []*entity.ActivityUserVoteStat, err error) {
+ voteStat = make([]*entity.ActivityUserVoteStat, 0)
+
+ actIDs := make([]int, 0)
+ for _, act := range activity_type.ActivityTypeList {
+ configType, err := ar.configRepo.GetConfigType(act)
+ if err == nil {
+ actIDs = append(actIDs, configType)
+ }
+ }
+
+ session := ar.data.DB.Select("user_id, COUNT(*) AS vote_count").Table("activity")
+ session.Where("cancelled = 0")
+ session.In("activity_type", actIDs)
+ session.Where("created_at >= ?", startTime)
+ session.Where("created_at <= ?", endTime)
+ session.GroupBy("user_id")
+ session.Desc("vote_count")
+ session.Limit(limit)
+ err = session.Find(&voteStat)
+ if err != nil {
+ err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
+ }
+ return
+}
diff --git a/internal/repo/auth/auth.go b/internal/repo/auth/auth.go
index 9f5a1813..b4bd1f19 100644
--- a/internal/repo/auth/auth.go
+++ b/internal/repo/auth/auth.go
@@ -10,6 +10,7 @@ import (
"github.com/answerdev/answer/internal/entity"
"github.com/answerdev/answer/internal/service/auth"
"github.com/segmentfault/pacman/errors"
+ "github.com/segmentfault/pacman/log"
)
// authRepo auth repository
@@ -42,6 +43,9 @@ func (ar *authRepo) SetUserCacheInfo(ctx context.Context, accessToken string, us
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
+ if err := ar.AddUserTokenMapping(ctx, userInfo.UserID, accessToken); err != nil {
+ log.Error(err)
+ }
return nil
}
@@ -130,6 +134,44 @@ func (ar *authRepo) RemoveBackyardUserCacheInfo(ctx context.Context, accessToken
return nil
}
+// AddUserTokenMapping add user token mapping
+func (ar *authRepo) AddUserTokenMapping(ctx context.Context, userID, accessToken string) (err error) {
+ key := constant.UserTokenMappingCacheKey + userID
+ resp, _ := ar.data.Cache.GetString(ctx, key)
+ mapping := make(map[string]bool, 0)
+ if len(resp) > 0 {
+ _ = json.Unmarshal([]byte(resp), &mapping)
+ }
+ mapping[accessToken] = true
+ content, _ := json.Marshal(mapping)
+ return ar.data.Cache.SetString(ctx, key, string(content), constant.UserTokenCacheTime)
+}
+
+// RemoveAllUserTokens Log out all users under this user id
+func (ar *authRepo) RemoveAllUserTokens(ctx context.Context, userID string) {
+ key := constant.UserTokenMappingCacheKey + userID
+ resp, _ := ar.data.Cache.GetString(ctx, key)
+ mapping := make(map[string]bool, 0)
+ if len(resp) > 0 {
+ _ = json.Unmarshal([]byte(resp), &mapping)
+ log.Debugf("find %d user tokens by user id %s", len(mapping), userID)
+ }
+
+ for token := range mapping {
+ if err := ar.RemoveUserCacheInfo(ctx, token); err != nil {
+ log.Error(err)
+ } else {
+ log.Debugf("del user %s token success")
+ }
+ }
+ if err := ar.RemoveUserStatus(ctx, userID); err != nil {
+ log.Error(err)
+ }
+ if err := ar.data.Cache.Del(ctx, key); err != nil {
+ log.Error(err)
+ }
+}
+
// NewAuthRepo new repository
func NewAuthRepo(data *data.Data) auth.AuthRepo {
return &authRepo{
diff --git a/internal/repo/captcha/captcha.go b/internal/repo/captcha/captcha.go
index 73ca1450..13f6d476 100644
--- a/internal/repo/captcha/captcha.go
+++ b/internal/repo/captcha/captcha.go
@@ -9,6 +9,7 @@ import (
"github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/internal/service/action"
"github.com/segmentfault/pacman/errors"
+ "github.com/segmentfault/pacman/log"
)
// captchaRepo captcha repository
@@ -65,7 +66,7 @@ func (cr *captchaRepo) SetCaptcha(ctx context.Context, key, captcha string) (err
func (cr *captchaRepo) GetCaptcha(ctx context.Context, key string) (captcha string, err error) {
captcha, err = cr.data.Cache.GetString(ctx, key)
if err != nil {
- err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
+ log.Debug(err)
}
// TODO: cache reflect should return empty when key not found
return captcha, nil
diff --git a/internal/repo/provider.go b/internal/repo/provider.go
index 9b078dae..7bc82aee 100644
--- a/internal/repo/provider.go
+++ b/internal/repo/provider.go
@@ -19,6 +19,7 @@ import (
"github.com/answerdev/answer/internal/repo/reason"
"github.com/answerdev/answer/internal/repo/report"
"github.com/answerdev/answer/internal/repo/revision"
+ "github.com/answerdev/answer/internal/repo/role"
"github.com/answerdev/answer/internal/repo/search_common"
"github.com/answerdev/answer/internal/repo/site_info"
"github.com/answerdev/answer/internal/repo/tag"
@@ -67,4 +68,8 @@ var ProviderSetRepo = wire.NewSet(
reason.NewReasonRepo,
site_info.NewSiteInfo,
notification.NewNotificationRepo,
+ role.NewRoleRepo,
+ role.NewUserRoleRelRepo,
+ role.NewRolePowerRelRepo,
+ role.NewPowerRepo,
)
diff --git a/internal/repo/question/question_repo.go b/internal/repo/question/question_repo.go
index 82686fe5..7969aeb1 100644
--- a/internal/repo/question/question_repo.go
+++ b/internal/repo/question/question_repo.go
@@ -2,6 +2,7 @@ package question
import (
"context"
+ "fmt"
"strings"
"time"
"unicode"
@@ -16,6 +17,7 @@ import (
"github.com/answerdev/answer/internal/schema"
questioncommon "github.com/answerdev/answer/internal/service/question_common"
"github.com/answerdev/answer/internal/service/unique"
+ "github.com/answerdev/answer/pkg/htmltext"
"github.com/segmentfault/pacman/errors"
)
@@ -173,6 +175,36 @@ func (qr *questionRepo) GetQuestionCount(ctx context.Context) (count int64, err
return
}
+func (qr *questionRepo) GetQuestionIDsPage(ctx context.Context, page, pageSize int) (questionIDList []*schema.SiteMapQuestionInfo, err error) {
+ questionIDList = make([]*schema.SiteMapQuestionInfo, 0)
+ rows := make([]*entity.Question, 0)
+ if page > 0 {
+ page = page - 1
+ } else {
+ page = 0
+ }
+ if pageSize == 0 {
+ pageSize = constant.DefaultPageSize
+ }
+ offset := page * pageSize
+ session := qr.data.DB.Table("question")
+ session = session.In("question.status", []int{entity.QuestionStatusAvailable, entity.QuestionStatusClosed})
+ session = session.Limit(pageSize, offset)
+ session = session.OrderBy("question.created_at asc")
+ err = session.Select("id,title,post_update_time").Find(&rows)
+ if err != nil {
+ return questionIDList, err
+ }
+ for _, question := range rows {
+ item := &schema.SiteMapQuestionInfo{}
+ item.ID = question.ID
+ item.Title = htmltext.UrlTitle(question.Title)
+ item.UpdateTime = fmt.Sprintf("%v", question.PostUpdateTime.UTC())
+ questionIDList = append(questionIDList, item)
+ }
+ return questionIDList, nil
+}
+
// GetQuestionPage get question page
func (qr *questionRepo) GetQuestionPage(ctx context.Context, page, pageSize int, question *entity.Question) (questionList []*entity.Question, total int64, err error) {
questionList = make([]*entity.Question, 0)
diff --git a/internal/repo/rank/user_rank_repo.go b/internal/repo/rank/user_rank_repo.go
index 13996817..56a0a42f 100644
--- a/internal/repo/rank/user_rank_repo.go
+++ b/internal/repo/rank/user_rank_repo.go
@@ -43,7 +43,7 @@ func (ur *UserRankRepo) TriggerUserRank(ctx context.Context,
if deltaRank < 0 {
// if user rank is lower than 1 after this action, then user rank will be set to 1 only.
var isReachMin bool
- isReachMin, err = ur.checkUserMinRank(ctx, session, userID, activityType)
+ isReachMin, err = ur.checkUserMinRank(ctx, session, userID, deltaRank)
if err != nil {
return false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
@@ -52,7 +52,7 @@ func (ur *UserRankRepo) TriggerUserRank(ctx context.Context,
if err != nil {
return false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
- return false, nil
+ return true, nil
}
} else {
isReachStandard, err = ur.checkUserTodayRank(ctx, session, userID, activityType)
diff --git a/internal/repo/repo_test/comment_repo_test.go b/internal/repo/repo_test/comment_repo_test.go
index c7ad1d41..b482cbf1 100644
--- a/internal/repo/repo_test/comment_repo_test.go
+++ b/internal/repo/repo_test/comment_repo_test.go
@@ -33,7 +33,6 @@ func Test_commentRepo_AddComment(t *testing.T) {
err = commentRepo.RemoveComment(context.TODO(), testCommentEntity.ID)
assert.NoError(t, err)
- return
}
func Test_commentRepo_GetCommentPage(t *testing.T) {
@@ -55,7 +54,6 @@ func Test_commentRepo_GetCommentPage(t *testing.T) {
err = commentRepo.RemoveComment(context.TODO(), testCommentEntity.ID)
assert.NoError(t, err)
- return
}
func Test_commentRepo_UpdateComment(t *testing.T) {
@@ -77,5 +75,4 @@ func Test_commentRepo_UpdateComment(t *testing.T) {
err = commentRepo.RemoveComment(context.TODO(), testCommentEntity.ID)
assert.NoError(t, err)
- return
}
diff --git a/internal/repo/repo_test/revision_repo_test.go b/internal/repo/repo_test/revision_repo_test.go
index fb6e0561..7d82891c 100644
--- a/internal/repo/repo_test/revision_repo_test.go
+++ b/internal/repo/repo_test/revision_repo_test.go
@@ -3,12 +3,13 @@ package repo_test
import (
"context"
"encoding/json"
+ "testing"
+
"github.com/answerdev/answer/internal/entity"
"github.com/answerdev/answer/internal/repo/question"
"github.com/answerdev/answer/internal/repo/revision"
"github.com/answerdev/answer/internal/repo/unique"
"github.com/stretchr/testify/assert"
- "testing"
)
var q = &entity.Question{
@@ -53,6 +54,7 @@ func Test_revisionRepo_AddRevision(t *testing.T) {
assert.NotEqual(t, "", q.ID)
content, err := json.Marshal(q)
+ assert.NoError(t, err)
// auto update false
rev := getRev(q.ID, q.Title, string(content))
err = revisionRepo.AddRevision(context.TODO(), rev, false)
diff --git a/internal/repo/repo_test/tag_repo_test.go b/internal/repo/repo_test/tag_repo_test.go
index d7b5e71c..f3e636fb 100644
--- a/internal/repo/repo_test/tag_repo_test.go
+++ b/internal/repo/repo_test/tag_repo_test.go
@@ -92,7 +92,7 @@ func Test_tagRepo_GetTagListByName(t *testing.T) {
tagOnce.Do(addTagList)
tagCommonRepo := tag_common.NewTagCommonRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource))
- gotTags, err := tagCommonRepo.GetTagListByName(context.TODO(), testTagList[0].SlugName, 1, false)
+ gotTags, err := tagCommonRepo.GetTagListByName(context.TODO(), testTagList[0].SlugName, false)
assert.NoError(t, err)
assert.Equal(t, testTagList[0].SlugName, gotTags[0].SlugName)
}
diff --git a/internal/repo/repo_test/user_backyard_repo_test.go b/internal/repo/repo_test/user_backyard_repo_test.go
index 7fe3be05..de2effda 100644
--- a/internal/repo/repo_test/user_backyard_repo_test.go
+++ b/internal/repo/repo_test/user_backyard_repo_test.go
@@ -20,7 +20,7 @@ func Test_userBackyardRepo_GetUserInfo(t *testing.T) {
func Test_userBackyardRepo_GetUserPage(t *testing.T) {
userBackyardRepo := user.NewUserBackyardRepo(testDataSource, auth.NewAuthRepo(testDataSource))
- got, total, err := userBackyardRepo.GetUserPage(context.TODO(), 1, 1, &entity.User{Username: "admin"}, "")
+ got, total, err := userBackyardRepo.GetUserPage(context.TODO(), 1, 1, &entity.User{Username: "admin"}, "", false)
assert.NoError(t, err)
assert.Equal(t, int64(1), total)
assert.Equal(t, "1", got[0].ID)
diff --git a/internal/repo/report/report_repo.go b/internal/repo/report/report_repo.go
index 08747246..3fa627f0 100644
--- a/internal/repo/report/report_repo.go
+++ b/internal/repo/report/report_repo.go
@@ -76,28 +76,27 @@ func (rr *reportRepo) GetReportListPage(ctx context.Context, dto schema.GetRepor
}
// GetByID get report by ID
-func (ar *reportRepo) GetByID(ctx context.Context, id string) (report entity.Report, exist bool, err error) {
- report = entity.Report{}
- exist, err = ar.data.DB.ID(id).Get(&report)
- return
-}
-
-// UpdateByID handle report by ID
-func (ar *reportRepo) UpdateByID(
- ctx context.Context,
- id string,
- handleData entity.Report,
-) (err error) {
- _, err = ar.data.DB.ID(id).Update(&handleData)
+func (rr *reportRepo) GetByID(ctx context.Context, id string) (report *entity.Report, exist bool, err error) {
+ report = &entity.Report{}
+ exist, err = rr.data.DB.ID(id).Get(report)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
-func (vr *reportRepo) GetReportCount(ctx context.Context) (count int64, err error) {
+// UpdateByID handle report by ID
+func (rr *reportRepo) UpdateByID(ctx context.Context, id string, handleData entity.Report) (err error) {
+ _, err = rr.data.DB.ID(id).Update(&handleData)
+ if err != nil {
+ err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
+ }
+ return
+}
+
+func (rr *reportRepo) GetReportCount(ctx context.Context) (count int64, err error) {
list := make([]*entity.Report, 0)
- count, err = vr.data.DB.Where("status =?", entity.ReportStatusPending).FindAndCount(&list)
+ count, err = rr.data.DB.Where("status =?", entity.ReportStatusPending).FindAndCount(&list)
if err != nil {
return count, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
diff --git a/internal/repo/role/power_repo.go b/internal/repo/role/power_repo.go
new file mode 100644
index 00000000..1c902e6c
--- /dev/null
+++ b/internal/repo/role/power_repo.go
@@ -0,0 +1,33 @@
+package role
+
+import (
+ "context"
+
+ "github.com/answerdev/answer/internal/base/data"
+ "github.com/answerdev/answer/internal/base/reason"
+ "github.com/answerdev/answer/internal/entity"
+ "github.com/answerdev/answer/internal/service/role"
+ "github.com/segmentfault/pacman/errors"
+)
+
+// powerRepo power repository
+type powerRepo struct {
+ data *data.Data
+}
+
+// NewPowerRepo new repository
+func NewPowerRepo(data *data.Data) role.PowerRepo {
+ return &powerRepo{
+ data: data,
+ }
+}
+
+// GetPowerList get list all
+func (pr *powerRepo) GetPowerList(ctx context.Context, power *entity.Power) (powerList []*entity.Power, err error) {
+ powerList = make([]*entity.Power, 0)
+ err = pr.data.DB.Find(powerList, power)
+ if err != nil {
+ err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
+ }
+ return
+}
diff --git a/internal/repo/role/role_power_rel_repo.go b/internal/repo/role/role_power_rel_repo.go
new file mode 100644
index 00000000..dc553eaa
--- /dev/null
+++ b/internal/repo/role/role_power_rel_repo.go
@@ -0,0 +1,34 @@
+package role
+
+import (
+ "context"
+
+ "github.com/answerdev/answer/internal/base/data"
+ "github.com/answerdev/answer/internal/base/reason"
+ "github.com/answerdev/answer/internal/service/role"
+ "github.com/segmentfault/pacman/errors"
+ "xorm.io/builder"
+)
+
+// rolePowerRelRepo rolePowerRel repository
+type rolePowerRelRepo struct {
+ data *data.Data
+}
+
+// NewRolePowerRelRepo new repository
+func NewRolePowerRelRepo(data *data.Data) role.RolePowerRelRepo {
+ return &rolePowerRelRepo{
+ data: data,
+ }
+}
+
+// GetRolePowerTypeList get role power type list
+func (rr *rolePowerRelRepo) GetRolePowerTypeList(ctx context.Context, roleID int) (powers []string, err error) {
+ powers = make([]string, 0)
+ err = rr.data.DB.Table("role_power_rel").
+ Cols("power_type").Where(builder.Eq{"role_id": roleID}).Find(&powers)
+ if err != nil {
+ err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
+ }
+ return
+}
diff --git a/internal/repo/role/role_repo.go b/internal/repo/role/role_repo.go
new file mode 100644
index 00000000..4534795e
--- /dev/null
+++ b/internal/repo/role/role_repo.go
@@ -0,0 +1,46 @@
+package role
+
+import (
+ "context"
+
+ "github.com/answerdev/answer/internal/base/data"
+ "github.com/answerdev/answer/internal/base/reason"
+ "github.com/answerdev/answer/internal/entity"
+ service "github.com/answerdev/answer/internal/service/role"
+ "github.com/segmentfault/pacman/errors"
+)
+
+// roleRepo role repository
+type roleRepo struct {
+ data *data.Data
+}
+
+// NewRoleRepo new repository
+func NewRoleRepo(data *data.Data) service.RoleRepo {
+ return &roleRepo{
+ data: data,
+ }
+}
+
+// GetRoleAllList get role list all
+func (rr *roleRepo) GetRoleAllList(ctx context.Context) (roleList []*entity.Role, err error) {
+ roleList = make([]*entity.Role, 0)
+ err = rr.data.DB.Find(&roleList)
+ if err != nil {
+ err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
+ }
+ return
+}
+
+// GetRoleAllMapping get role all mapping
+func (rr *roleRepo) GetRoleAllMapping(ctx context.Context) (roleMapping map[int]*entity.Role, err error) {
+ roleList, err := rr.GetRoleAllList(ctx)
+ if err != nil {
+ return nil, err
+ }
+ roleMapping = make(map[int]*entity.Role, 0)
+ for _, role := range roleList {
+ roleMapping[role.ID] = role
+ }
+ return roleMapping, nil
+}
diff --git a/internal/repo/role/user_role_rel_repo.go b/internal/repo/role/user_role_rel_repo.go
new file mode 100644
index 00000000..85d6564b
--- /dev/null
+++ b/internal/repo/role/user_role_rel_repo.go
@@ -0,0 +1,83 @@
+package role
+
+import (
+ "context"
+
+ "github.com/answerdev/answer/internal/base/data"
+ "github.com/answerdev/answer/internal/base/reason"
+ "github.com/answerdev/answer/internal/entity"
+ "github.com/answerdev/answer/internal/service/role"
+ "github.com/segmentfault/pacman/errors"
+ "xorm.io/builder"
+ "xorm.io/xorm"
+)
+
+// userRoleRelRepo userRoleRel repository
+type userRoleRelRepo struct {
+ data *data.Data
+}
+
+// NewUserRoleRelRepo new repository
+func NewUserRoleRelRepo(data *data.Data) role.UserRoleRelRepo {
+ return &userRoleRelRepo{
+ data: data,
+ }
+}
+
+// SaveUserRoleRel save user role rel
+func (ur *userRoleRelRepo) SaveUserRoleRel(ctx context.Context, userID string, roleID int) (err error) {
+ _, err = ur.data.DB.Transaction(func(session *xorm.Session) (interface{}, error) {
+ item := &entity.UserRoleRel{UserID: userID}
+ exist, err := ur.data.DB.Get(item)
+ if err != nil {
+ return nil, err
+ }
+ if exist {
+ item.RoleID = roleID
+ _, err = ur.data.DB.ID(item.ID).Update(item)
+ } else {
+ _, err = ur.data.DB.Insert(&entity.UserRoleRel{UserID: userID, RoleID: roleID})
+ }
+ if err != nil {
+ return nil, err
+ }
+ return nil, nil
+ })
+ if err != nil {
+ err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
+ }
+ return
+}
+
+// GetUserRoleRelList get user role all
+func (ur *userRoleRelRepo) GetUserRoleRelList(ctx context.Context, userIDs []string) (
+ userRoleRelList []*entity.UserRoleRel, err error) {
+ userRoleRelList = make([]*entity.UserRoleRel, 0)
+ err = ur.data.DB.In("user_id", userIDs).Find(&userRoleRelList)
+ if err != nil {
+ err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
+ }
+ return
+}
+
+// GetUserRoleRelListByRoleID get user role all by role id
+func (ur *userRoleRelRepo) GetUserRoleRelListByRoleID(ctx context.Context, roleIDs []int) (
+ userRoleRelList []*entity.UserRoleRel, err error) {
+ userRoleRelList = make([]*entity.UserRoleRel, 0)
+ err = ur.data.DB.In("role_id", roleIDs).Find(&userRoleRelList)
+ if err != nil {
+ err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
+ }
+ return
+}
+
+// GetUserRoleRel get user role
+func (ur *userRoleRelRepo) GetUserRoleRel(ctx context.Context, userID string) (
+ rolePowerRel *entity.UserRoleRel, exist bool, err error) {
+ rolePowerRel = &entity.UserRoleRel{}
+ exist, err = ur.data.DB.Where(builder.Eq{"user_id": userID}).Get(rolePowerRel)
+ if err != nil {
+ err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
+ }
+ return
+}
diff --git a/internal/repo/search_common/search_repo.go b/internal/repo/search_common/search_repo.go
index 666ba4fd..5c5bc25f 100644
--- a/internal/repo/search_common/search_repo.go
+++ b/internal/repo/search_common/search_repo.go
@@ -69,9 +69,8 @@ func NewSearchRepo(data *data.Data, uniqueIDRepo unique.UniqueIDRepo, userCommon
// SearchContents search question and answer data
func (sr *searchRepo) SearchContents(ctx context.Context, words []string, tagIDs []string, userID string, votes int, page, size int, order string) (resp []schema.SearchResp, total int64, err error) {
- if words = filterWords(words); len(words) == 0 {
- return
- }
+ words = filterWords(words)
+
var (
b *builder.Builder
ub *builder.Builder
@@ -80,9 +79,14 @@ func (sr *searchRepo) SearchContents(ctx context.Context, words []string, tagIDs
argsQ = []interface{}{}
argsA = []interface{}{}
)
+
if order == "relevance" {
- qfs, argsQ = addRelevanceField([]string{"title", "original_text"}, words, qfs)
- afs, argsA = addRelevanceField([]string{"`answer`.`original_text`"}, words, afs)
+ if len(words) > 0 {
+ qfs, argsQ = addRelevanceField([]string{"title", "original_text"}, words, qfs)
+ afs, argsA = addRelevanceField([]string{"`answer`.`original_text`"}, words, afs)
+ } else {
+ order = "newest"
+ }
}
b = builder.MySQL().Select(qfs...).From("`question`")
diff --git a/internal/repo/site_info/siteinfo_repo.go b/internal/repo/site_info/siteinfo_repo.go
index bcb90199..bee8ae50 100644
--- a/internal/repo/site_info/siteinfo_repo.go
+++ b/internal/repo/site_info/siteinfo_repo.go
@@ -2,12 +2,15 @@ package site_info
import (
"context"
+ "encoding/json"
+ "github.com/answerdev/answer/internal/base/constant"
"github.com/answerdev/answer/internal/base/data"
"github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/internal/entity"
"github.com/answerdev/answer/internal/service/siteinfo_common"
"github.com/segmentfault/pacman/errors"
+ "github.com/segmentfault/pacman/log"
"xorm.io/builder"
)
@@ -23,32 +26,55 @@ func NewSiteInfo(data *data.Data) siteinfo_common.SiteInfoRepo {
// SaveByType save site setting by type
func (sr *siteInfoRepo) SaveByType(ctx context.Context, siteType string, data *entity.SiteInfo) (err error) {
- var (
- old = &entity.SiteInfo{}
- exist bool
- )
- exist, _ = sr.data.DB.Where(builder.Eq{"type": siteType}).Get(old)
+ old := &entity.SiteInfo{}
+ exist, err := sr.data.DB.Where(builder.Eq{"type": siteType}).Get(old)
+ if err != nil {
+ return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
+ }
if exist {
_, err = sr.data.DB.ID(old.ID).Update(data)
- if err != nil {
- err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
- }
- return
+ } else {
+ _, err = sr.data.DB.Insert(data)
}
-
- _, err = sr.data.DB.Insert(data)
if err != nil {
- err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
+ return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
+ sr.setCache(ctx, siteType, data)
return
}
// GetByType get site info by type
func (sr *siteInfoRepo) GetByType(ctx context.Context, siteType string) (siteInfo *entity.SiteInfo, exist bool, err error) {
+ siteInfo = sr.getCache(ctx, siteType)
+ if siteInfo != nil {
+ return siteInfo, true, nil
+ }
siteInfo = &entity.SiteInfo{}
exist, err = sr.data.DB.Where(builder.Eq{"type": siteType}).Get(siteInfo)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
+ if exist {
+ sr.setCache(ctx, siteType, siteInfo)
+ }
return
}
+
+func (sr *siteInfoRepo) getCache(ctx context.Context, siteType string) (siteInfo *entity.SiteInfo) {
+ siteInfo = &entity.SiteInfo{}
+ siteInfoCache, err := sr.data.Cache.GetString(ctx, constant.SiteInfoCacheKey+siteType)
+ if err != nil {
+ return nil
+ }
+ _ = json.Unmarshal([]byte(siteInfoCache), siteInfo)
+ return siteInfo
+}
+
+func (sr *siteInfoRepo) setCache(ctx context.Context, siteType string, siteInfo *entity.SiteInfo) {
+ siteInfoCache, _ := json.Marshal(siteInfo)
+ err := sr.data.Cache.SetString(ctx,
+ constant.SiteInfoCacheKey+siteType, string(siteInfoCache), constant.SiteInfoCacheTime)
+ if err != nil {
+ log.Error(err)
+ }
+}
diff --git a/internal/repo/tag_common/tag_common_repo.go b/internal/repo/tag_common/tag_common_repo.go
index 99f16cc4..bfaa24dc 100644
--- a/internal/repo/tag_common/tag_common_repo.go
+++ b/internal/repo/tag_common/tag_common_repo.go
@@ -55,7 +55,7 @@ func (tr *tagCommonRepo) GetTagBySlugName(ctx context.Context, slugName string)
}
// GetTagListByName get tag list all like name
-func (tr *tagCommonRepo) GetTagListByName(ctx context.Context, name string, limit int, hasReserved bool) (tagList []*entity.Tag, err error) {
+func (tr *tagCommonRepo) GetTagListByName(ctx context.Context, name string, hasReserved bool) (tagList []*entity.Tag, err error) {
tagList = make([]*entity.Tag, 0)
cond := &entity.Tag{}
session := tr.data.DB.Where("")
@@ -65,13 +65,13 @@ func (tr *tagCommonRepo) GetTagListByName(ctx context.Context, name string, limi
cond.Recommend = true
}
session.Where(builder.Eq{"status": entity.TagStatusAvailable})
- session.Limit(limit).Asc("slug_name")
- if !hasReserved {
- cond.Reserved = false
- session.UseBool("recommend", "reserved")
- } else {
- session.UseBool("recommend")
- }
+ session.Asc("slug_name")
+ // if !hasReserved {
+ // cond.Reserved = false
+ // session.UseBool("recommend", "reserved")
+ // } else {
+ session.UseBool("recommend")
+ // }
err = session.OrderBy("recommend desc,reserved desc,id desc").Find(&tagList, cond)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
diff --git a/internal/repo/user/user_backyard_repo.go b/internal/repo/user/user_backyard_repo.go
index 71ae7fca..9c050999 100644
--- a/internal/repo/user/user_backyard_repo.go
+++ b/internal/repo/user/user_backyard_repo.go
@@ -3,10 +3,7 @@ package user
import (
"context"
"encoding/json"
- "net/mail"
- "strings"
"time"
- "unicode"
"xorm.io/builder"
@@ -64,6 +61,24 @@ func (ur *userBackyardRepo) UpdateUserStatus(ctx context.Context, userID string,
return
}
+// AddUser add user
+func (ur *userBackyardRepo) AddUser(ctx context.Context, user *entity.User) (err error) {
+ _, err = ur.data.DB.Insert(user)
+ if err != nil {
+ err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
+ }
+ return
+}
+
+// UpdateUserPassword update user password
+func (ur *userBackyardRepo) UpdateUserPassword(ctx context.Context, userID string, password string) (err error) {
+ _, err = ur.data.DB.ID(userID).Update(&entity.User{Pass: password})
+ if err != nil {
+ return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
+ }
+ return
+}
+
// GetUserInfo get user info
func (ur *userBackyardRepo) GetUserInfo(ctx context.Context, userID string) (user *entity.User, exist bool, err error) {
user = &entity.User{}
@@ -74,50 +89,39 @@ func (ur *userBackyardRepo) GetUserInfo(ctx context.Context, userID string) (use
return
}
+// GetUserInfoByEmail get user info
+func (ur *userBackyardRepo) GetUserInfoByEmail(ctx context.Context, email string) (user *entity.User, exist bool, err error) {
+ userInfo := &entity.User{}
+ exist, err = ur.data.DB.Where("e_mail = ?", email).
+ Where("status != ?", entity.UserStatusDeleted).Get(userInfo)
+ if err != nil {
+ err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
+ }
+ return
+}
+
// GetUserPage get user page
-func (ur *userBackyardRepo) GetUserPage(ctx context.Context, page, pageSize int, user *entity.User, query string) (users []*entity.User, total int64, err error) {
+func (ur *userBackyardRepo) GetUserPage(ctx context.Context, page, pageSize int, user *entity.User,
+ usernameOrDisplayName string, isStaff bool) (users []*entity.User, total int64, err error) {
users = make([]*entity.User, 0)
session := ur.data.DB.NewSession()
switch user.Status {
case entity.UserStatusDeleted:
- session.Desc("deleted_at")
+ session.Desc("user.deleted_at")
case entity.UserStatusSuspended:
- session.Desc("suspended_at")
+ session.Desc("user.suspended_at")
default:
- session.Desc("created_at")
+ session.Desc("user.created_at")
}
- if len(query) > 0 {
- if email, e := mail.ParseAddress(query); e == nil {
- session.And(builder.Eq{"e_mail": email.Address})
- } else {
- var (
- idSearch = false
- id = ""
- )
-
- if strings.Contains(query, "user:") {
- idSearch = true
- id = strings.TrimSpace(strings.TrimPrefix(query, "user:"))
- for _, r := range id {
- if !unicode.IsDigit(r) {
- idSearch = false
- break
- }
- }
- }
-
- if idSearch {
- session.And(builder.Eq{
- "id": id,
- })
- } else {
- session.And(builder.Or(
- builder.Like{"username", query},
- builder.Like{"display_name", query},
- ))
- }
- }
+ if len(usernameOrDisplayName) > 0 {
+ session.And(builder.Or(
+ builder.Like{"user.username", usernameOrDisplayName},
+ builder.Like{"user.display_name", usernameOrDisplayName},
+ ))
+ }
+ if isStaff {
+ session.Join("INNER", "user_role_rel", "user.id = user_role_rel.user_id AND user_role_rel.role_id > 1")
}
total, err = pager.Help(page, pageSize, &users, user, session)
diff --git a/internal/repo/user/user_repo.go b/internal/repo/user/user_repo.go
index 10324baa..e5a33314 100644
--- a/internal/repo/user/user_repo.go
+++ b/internal/repo/user/user_repo.go
@@ -28,7 +28,7 @@ func NewUserRepo(data *data.Data, configRepo config.ConfigRepo) usercommon.UserR
// AddUser add user
func (ur *userRepo) AddUser(ctx context.Context, user *entity.User) (err error) {
- _, err = ur.data.DB.UseBool("is_admin").Insert(user)
+ _, err = ur.data.DB.Insert(user)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
@@ -151,7 +151,8 @@ func (ur *userRepo) GetByUsername(ctx context.Context, username string) (userInf
// GetByEmail get user by email
func (ur *userRepo) GetByEmail(ctx context.Context, email string) (userInfo *entity.User, exist bool, err error) {
userInfo = &entity.User{}
- exist, err = ur.data.DB.Where("e_mail = ?", email).Get(userInfo)
+ exist, err = ur.data.DB.Where("e_mail = ?", email).
+ Where("status != ?", entity.UserStatusDeleted).Get(userInfo)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
diff --git a/internal/router/answer_api_router.go b/internal/router/answer_api_router.go
index 29d6cea4..9e364c3e 100644
--- a/internal/router/answer_api_router.go
+++ b/internal/router/answer_api_router.go
@@ -30,6 +30,7 @@ type AnswerAPIRouter struct {
dashboardController *controller.DashboardController
uploadController *controller.UploadController
activityController *controller.ActivityController
+ roleController *controller_backyard.RoleController
}
func NewAnswerAPIRouter(
@@ -56,6 +57,7 @@ func NewAnswerAPIRouter(
dashboardController *controller.DashboardController,
uploadController *controller.UploadController,
activityController *controller.ActivityController,
+ roleController *controller_backyard.RoleController,
) *AnswerAPIRouter {
return &AnswerAPIRouter{
langController: langController,
@@ -81,32 +83,38 @@ func NewAnswerAPIRouter(
dashboardController: dashboardController,
uploadController: uploadController,
activityController: activityController,
+ roleController: roleController,
}
}
-func (a *AnswerAPIRouter) RegisterUnAuthAnswerAPIRouter(r *gin.RouterGroup) {
+func (a *AnswerAPIRouter) RegisterMustUnAuthAnswerAPIRouter(r *gin.RouterGroup) {
// i18n
r.GET("/language/config", a.langController.GetLangMapping)
r.GET("/language/options", a.langController.GetUserLangOptions)
- // comment
- r.GET("/comment/page", a.commentController.GetCommentWithPage)
- r.GET("/personal/comment/page", a.commentController.GetCommentPersonalWithPage)
- r.GET("/comment", a.commentController.GetComment)
+ //siteinfo
+ r.GET("/siteinfo", a.siteinfoController.GetSiteInfo)
+ r.GET("/siteinfo/legal", a.siteinfoController.GetSiteLegalInfo)
// user
- r.GET("/user/info", a.userController.GetUserInfoByUserID)
- r.GET("/user/action/record", a.userController.ActionRecord)
r.POST("/user/login/email", a.userController.UserEmailLogin)
r.POST("/user/register/email", a.userController.UserRegisterByEmail)
+ r.GET("/user/register/captcha", a.userController.UserRegisterCaptcha)
r.POST("/user/email/verification", a.userController.UserVerifyEmail)
+ r.PUT("/user/email", a.userController.UserChangeEmailVerify)
+ r.GET("/user/action/record", a.userController.ActionRecord)
r.POST("/user/password/reset", a.userController.RetrievePassWord)
r.POST("/user/password/replacement", a.userController.UseRePassWord)
- r.GET("/personal/user/info", a.userController.GetOtherUserInfoByUsername)
- r.POST("/user/email/verification/send", a.userController.UserVerifyEmailSend)
+ r.GET("/user/info", a.userController.GetUserInfoByUserID)
+}
+
+func (a *AnswerAPIRouter) RegisterUnAuthAnswerAPIRouter(r *gin.RouterGroup) {
+ // user
r.GET("/user/logout", a.userController.UserLogout)
- r.PUT("/user/email", a.userController.UserChangeEmailVerify)
r.POST("/user/email/change/code", a.userController.UserChangeEmailSendCode)
+ r.POST("/user/email/verification/send", a.userController.UserVerifyEmailSend)
+ r.GET("/personal/user/info", a.userController.GetOtherUserInfoByUsername)
+ r.GET("/user/ranking", a.userController.UserRanking)
//answer
r.GET("/answer/info", a.answerController.Get)
@@ -121,6 +129,11 @@ func (a *AnswerAPIRouter) RegisterUnAuthAnswerAPIRouter(r *gin.RouterGroup) {
r.GET("/personal/qa/top", a.questionController.UserTop)
r.GET("/personal/question/page", a.questionController.UserList)
+ // comment
+ r.GET("/comment/page", a.commentController.GetCommentWithPage)
+ r.GET("/personal/comment/page", a.commentController.GetCommentPersonalWithPage)
+ r.GET("/comment", a.commentController.GetComment)
+
//revision
r.GET("/revisions", a.revisionController.GetRevisionList)
@@ -136,11 +149,6 @@ func (a *AnswerAPIRouter) RegisterUnAuthAnswerAPIRouter(r *gin.RouterGroup) {
//rank
r.GET("/personal/rank/page", a.rankController.GetRankPersonalWithPage)
-
- //siteinfo
- r.GET("/siteinfo", a.siteinfoController.GetSiteInfo)
- r.GET("/siteinfo/legal", a.siteinfoController.GetSiteLegalInfo)
-
}
func (a *AnswerAPIRouter) RegisterAnswerAPIRouter(r *gin.RouterGroup) {
@@ -180,6 +188,7 @@ func (a *AnswerAPIRouter) RegisterAnswerAPIRouter(r *gin.RouterGroup) {
r.PUT("/question", a.questionController.UpdateQuestion)
r.DELETE("/question", a.questionController.RemoveQuestion)
r.PUT("/question/status", a.questionController.CloseQuestion)
+ r.PUT("/question/reopen", a.questionController.ReopenQuestion)
r.GET("/question/similar", a.questionController.SearchByTitleLike)
// answer
@@ -229,6 +238,9 @@ func (a *AnswerAPIRouter) RegisterAnswerCmsAPIRouter(r *gin.RouterGroup) {
// user
r.GET("/users/page", a.backyardUserController.GetUserPage)
r.PUT("/user/status", a.backyardUserController.UpdateUserStatus)
+ r.PUT("/user/role", a.backyardUserController.UpdateUserRole)
+ r.POST("/user", a.backyardUserController.AddUser)
+ r.PUT("/user/password", a.backyardUserController.UpdateUserPassword)
// reason
r.GET("/reasons", a.reasonController.Reasons)
@@ -245,14 +257,25 @@ func (a *AnswerAPIRouter) RegisterAnswerCmsAPIRouter(r *gin.RouterGroup) {
r.GET("/siteinfo/branding", a.siteInfoController.GetSiteBranding)
r.GET("/siteinfo/write", a.siteInfoController.GetSiteWrite)
r.GET("/siteinfo/legal", a.siteInfoController.GetSiteLegal)
+ r.GET("/siteinfo/seo", a.siteInfoController.GetSeo)
+ r.GET("/siteinfo/login", a.siteInfoController.GetSiteLogin)
+ r.GET("/siteinfo/custom-css-html", a.siteInfoController.GetSiteCustomCssHTML)
+ r.GET("/siteinfo/theme", a.siteInfoController.GetSiteTheme)
r.PUT("/siteinfo/general", a.siteInfoController.UpdateGeneral)
r.PUT("/siteinfo/interface", a.siteInfoController.UpdateInterface)
r.PUT("/siteinfo/branding", a.siteInfoController.UpdateBranding)
r.PUT("/siteinfo/write", a.siteInfoController.UpdateSiteWrite)
r.PUT("/siteinfo/legal", a.siteInfoController.UpdateSiteLegal)
+ r.PUT("/siteinfo/login", a.siteInfoController.UpdateSiteLogin)
+ r.PUT("/siteinfo/custom-css-html", a.siteInfoController.UpdateSiteCustomCssHTML)
+ r.PUT("/siteinfo/theme", a.siteInfoController.SaveSiteTheme)
+ r.PUT("/siteinfo/seo", a.siteInfoController.UpdateSeo)
r.GET("/setting/smtp", a.siteInfoController.GetSMTPConfig)
r.PUT("/setting/smtp", a.siteInfoController.UpdateSMTPConfig)
- //dashboard
+ // dashboard
r.GET("/dashboard", a.dashboardController.DashboardInfo)
+
+ // roles
+ r.GET("/roles", a.roleController.GetRoleList)
}
diff --git a/internal/router/provider.go b/internal/router/provider.go
index 08df9ce5..f705c973 100644
--- a/internal/router/provider.go
+++ b/internal/router/provider.go
@@ -3,4 +3,4 @@ package router
import "github.com/google/wire"
// ProviderSetRouter is providers.
-var ProviderSetRouter = wire.NewSet(NewAnswerAPIRouter, NewSwaggerRouter, NewStaticRouter, NewUIRouter)
+var ProviderSetRouter = wire.NewSet(NewAnswerAPIRouter, NewSwaggerRouter, NewStaticRouter, NewUIRouter, NewTemplateRouter)
diff --git a/internal/router/template_router.go b/internal/router/template_router.go
new file mode 100644
index 00000000..3ee5871e
--- /dev/null
+++ b/internal/router/template_router.go
@@ -0,0 +1,49 @@
+package router
+
+import (
+ "github.com/answerdev/answer/internal/controller"
+ templaterender "github.com/answerdev/answer/internal/controller/template_render"
+ "github.com/answerdev/answer/internal/controller_backyard"
+ "github.com/gin-gonic/gin"
+)
+
+type TemplateRouter struct {
+ templateController *controller.TemplateController
+ templateRenderController *templaterender.TemplateRenderController
+ siteInfoController *controller_backyard.SiteInfoController
+}
+
+func NewTemplateRouter(
+ templateController *controller.TemplateController,
+ templateRenderController *templaterender.TemplateRenderController,
+ siteInfoController *controller_backyard.SiteInfoController,
+
+) *TemplateRouter {
+ return &TemplateRouter{
+ templateController: templateController,
+ templateRenderController: templateRenderController,
+ siteInfoController: siteInfoController,
+ }
+}
+
+// TemplateRouter template router
+func (a *TemplateRouter) RegisterTemplateRouter(r *gin.RouterGroup) {
+ r.GET("/sitemap.xml", a.templateController.Sitemap)
+ r.GET("/sitemap/:page", a.templateController.SitemapPage)
+
+ r.GET("/robots.txt", a.siteInfoController.GetRobots)
+ r.GET("/custom.css", a.siteInfoController.GetCss)
+
+ r.GET("/", a.templateController.Index)
+ r.GET("/index", a.templateController.Index)
+
+ r.GET("/questions", a.templateController.QuestionList)
+ r.GET("/questions/:id", a.templateController.QuestionInfo)
+ r.GET("/questions/:id/:title", a.templateController.QuestionInfo)
+ r.GET("/questions/:id/:title/:answerid", a.templateController.QuestionInfo)
+
+ r.GET("/tags", a.templateController.TagList)
+ r.GET("/tags/:tag", a.templateController.TagInfo)
+ r.GET("/users/:username", a.templateController.UserInfo)
+ r.GET("/404", a.templateController.Page404)
+}
diff --git a/internal/router/ui.go b/internal/router/ui.go
index 6f0e174f..2654a37e 100644
--- a/internal/router/ui.go
+++ b/internal/router/ui.go
@@ -7,6 +7,9 @@ import (
"net/http"
"os"
+ "github.com/answerdev/answer/internal/controller"
+ "github.com/answerdev/answer/internal/service/siteinfo_common"
+ "github.com/answerdev/answer/pkg/htmltext"
"github.com/answerdev/answer/ui"
"github.com/gin-gonic/gin"
"github.com/segmentfault/pacman/log"
@@ -18,11 +21,19 @@ const UIStaticPath = "build/static"
// UIRouter is an interface that provides ui static file routers
type UIRouter struct {
+ siteInfoController *controller.SiteinfoController
+ siteInfoService *siteinfo_common.SiteInfoCommonService
}
// NewUIRouter creates a new UIRouter instance with the embed resources
-func NewUIRouter() *UIRouter {
- return &UIRouter{}
+func NewUIRouter(
+ siteInfoController *controller.SiteinfoController,
+ siteInfoService *siteinfo_common.SiteInfoCommonService,
+) *UIRouter {
+ return &UIRouter{
+ siteInfoController: siteInfoController,
+ siteInfoService: siteInfoService,
+ }
}
// _resource is an interface that provides static file, it's a private interface
@@ -72,10 +83,22 @@ func (a *UIRouter) Register(r *gin.Engine) {
filePath := ""
switch urlPath {
case "/favicon.ico":
- c.Header("content-type", "image/vnd.microsoft.icon")
- filePath = UIRootFilePath + urlPath
+ branding, err := a.siteInfoService.GetSiteBranding(c)
+ if err != nil {
+ log.Error(err)
+ }
+ if branding.Favicon != "" {
+ c.String(http.StatusOK, htmltext.GetPicByUrl(branding.Favicon))
+ return
+ } else {
+ c.Header("content-type", "image/vnd.microsoft.icon")
+ filePath = UIRootFilePath + urlPath
+
+ }
case "/manifest.json":
- filePath = UIRootFilePath + urlPath
+ // filePath = UIRootFilePath + urlPath
+ a.siteInfoController.GetManifestJson(c)
+ return
case "/install":
// if answer is running by run command user can not access install page.
c.Redirect(http.StatusFound, "/")
diff --git a/internal/router/ui_test.go b/internal/router/ui_test.go
deleted file mode 100644
index 1ec160e9..00000000
--- a/internal/router/ui_test.go
+++ /dev/null
@@ -1,36 +0,0 @@
-package router
-
-import (
- "github.com/gin-gonic/gin"
- "github.com/stretchr/testify/assert"
- "net/http"
- "net/http/httptest"
- "testing"
-)
-
-func TestUIRouter_Register(t *testing.T) {
- r := gin.Default()
-
- NewUIRouter().Register(r)
-
- w := httptest.NewRecorder()
- req, _ := http.NewRequest("GET", "/", nil)
-
- r.ServeHTTP(w, req)
-
- assert.Equal(t, http.StatusOK, w.Code)
-}
-
-func TestUIRouter_Static(t *testing.T) {
- r := gin.Default()
-
- NewUIRouter().Register(r)
-
- w := httptest.NewRecorder()
- req, _ := http.NewRequest("GET", "/static/version.txt", nil)
-
- r.ServeHTTP(w, req)
-
- assert.Equal(t, http.StatusOK, w.Code)
- assert.Equal(t, "OK", w.Body.String())
-}
diff --git a/internal/schema/activity.go b/internal/schema/activity.go
index 1bfb7f39..6e775c88 100644
--- a/internal/schema/activity.go
+++ b/internal/schema/activity.go
@@ -39,18 +39,18 @@ type ActObjectTimeline struct {
ObjectType string `json:"object_type"`
Cancelled bool `json:"cancelled"`
CancelledAt int64 `json:"cancelled_at"`
- UserID string `json:"-"`
+ UserID string `json:"id"`
}
// ActObjectInfo act object info
type ActObjectInfo struct {
- ObjectType string `json:"object_type"`
Title string `json:"title"`
+ ObjectType string `json:"object_type"`
QuestionID string `json:"question_id"`
AnswerID string `json:"answer_id"`
- MainTagSlugName string `json:"main_tag_slug_name"`
Username string `json:"username"`
DisplayName string `json:"display_name"`
+ MainTagSlugName string `json:"main_tag_slug_name"`
}
// GetObjectTimelineDetailReq get object timeline detail request
diff --git a/internal/schema/answer_schema.go b/internal/schema/answer_schema.go
index aaac3558..c6f47485 100644
--- a/internal/schema/answer_schema.go
+++ b/internal/schema/answer_schema.go
@@ -1,5 +1,10 @@
package schema
+import (
+ "github.com/answerdev/answer/internal/base/validator"
+ "github.com/answerdev/answer/pkg/converter"
+)
+
// RemoveAnswerReq delete answer request
type RemoveAnswerReq struct {
// answer id
@@ -21,6 +26,11 @@ type AnswerAddReq struct {
UserID string `json:"-" ` // user_id
}
+func (req *AnswerAddReq) Check() (errFields []*validator.FormErrorField, err error) {
+ req.HTML = converter.Markdown2HTML(req.Content)
+ return nil, nil
+}
+
type AnswerUpdateReq struct {
ID string `json:"id"` // id
QuestionID string `json:"question_id" ` // question_id
@@ -34,6 +44,11 @@ type AnswerUpdateReq struct {
CanEdit bool `json:"-"`
}
+func (req *AnswerUpdateReq) Check() (errFields []*validator.FormErrorField, err error) {
+ req.HTML = converter.Markdown2HTML(req.Content)
+ return nil, nil
+}
+
// AnswerUpdateResp answer update resp
type AnswerUpdateResp struct {
WaitForReview bool `json:"wait_for_review"`
diff --git a/internal/schema/backyard_user_schema.go b/internal/schema/backyard_user_schema.go
index de158146f..57f3f183 100644
--- a/internal/schema/backyard_user_schema.go
+++ b/internal/schema/backyard_user_schema.go
@@ -30,6 +30,8 @@ type GetUserPageReq struct {
Query string `validate:"omitempty,gt=0,lte=100" form:"query"`
// user status
Status string `validate:"omitempty,oneof=suspended deleted inactive" form:"status"`
+ // staff, if staff is true means query admin or moderator
+ Staff bool `validate:"omitempty" form:"staff"`
}
func (r *GetUserPageReq) IsSuspended() bool { return r.Status == UserSuspended }
@@ -58,6 +60,10 @@ type GetUserPageResp struct {
DisplayName string `json:"display_name"`
// avatar
Avatar string `json:"avatar"`
+ // role id
+ RoleID int `json:"role_id"`
+ // role name
+ RoleName string `json:"role_name"`
}
// GetUserInfoReq get user request
@@ -68,3 +74,28 @@ type GetUserInfoReq struct {
// GetUserInfoResp get user response
type GetUserInfoResp struct {
}
+
+// UpdateUserRoleReq update user role request
+type UpdateUserRoleReq struct {
+ // user id
+ UserID string `validate:"required" json:"user_id"`
+ // role id
+ RoleID int `validate:"required" json:"role_id"`
+ // login user id
+ LoginUserID string `json:"-"`
+}
+
+// AddUserReq add user request
+type AddUserReq struct {
+ DisplayName string `validate:"required,gt=4,lte=30" json:"display_name"`
+ Email string `validate:"required,email,gt=0,lte=500" json:"email"`
+ Password string `validate:"required,gte=8,lte=32" json:"password"`
+ LoginUserID string `json:"-"`
+}
+
+// UpdateUserPasswordReq update user password request
+type UpdateUserPasswordReq struct {
+ UserID string `validate:"required" json:"user_id"`
+ Password string `validate:"required,gte=8,lte=32" json:"password"`
+ LoginUserID string `json:"-"`
+}
diff --git a/internal/schema/comment_schema.go b/internal/schema/comment_schema.go
index cf49edc8..05b3e18a 100644
--- a/internal/schema/comment_schema.go
+++ b/internal/schema/comment_schema.go
@@ -1,7 +1,9 @@
package schema
import (
+ "github.com/answerdev/answer/internal/base/validator"
"github.com/answerdev/answer/internal/entity"
+ "github.com/answerdev/answer/pkg/converter"
"github.com/jinzhu/copier"
)
@@ -27,6 +29,11 @@ type AddCommentReq struct {
CanDelete bool `json:"-"`
}
+func (req *AddCommentReq) Check() (errFields []*validator.FormErrorField, err error) {
+ req.ParsedText = converter.Markdown2HTML(req.OriginalText)
+ return nil, nil
+}
+
// RemoveCommentReq remove comment
type RemoveCommentReq struct {
// comment id
@@ -47,6 +54,11 @@ type UpdateCommentReq struct {
UserID string `json:"-"`
}
+func (req *UpdateCommentReq) Check() (errFields []*validator.FormErrorField, err error) {
+ req.ParsedText = converter.Markdown2HTML(req.OriginalText)
+ return nil, nil
+}
+
// GetCommentListReq get comment list all request
type GetCommentListReq struct {
// user id
diff --git a/internal/schema/question_schema.go b/internal/schema/question_schema.go
index f9be0293..777bb000 100644
--- a/internal/schema/question_schema.go
+++ b/internal/schema/question_schema.go
@@ -1,19 +1,29 @@
package schema
+import (
+ "github.com/answerdev/answer/internal/base/validator"
+ "github.com/answerdev/answer/pkg/converter"
+)
+
+const (
+ SitemapMaxSize = 50000
+ SitemapCachekey = "answer@sitemap"
+ SitemapPageCachekey = "answer@sitemap@page%d"
+)
+
// RemoveQuestionReq delete question request
type RemoveQuestionReq struct {
// question id
- ID string `validate:"required" comment:"question id" json:"id"`
+ ID string `validate:"required" json:"id"`
UserID string `json:"-" ` // user_id
IsAdmin bool `json:"-"`
}
type CloseQuestionReq struct {
- ID string `validate:"required" comment:"question id" json:"id"`
- UserID string `json:"-" ` // user_id
- CloseType int `json:"close_type" ` // close_type
- CloseMsg string `json:"close_msg" ` // close_type
- IsAdmin bool `json:"-"`
+ ID string `validate:"required" json:"id"`
+ CloseType int `json:"close_type"` // close_type
+ CloseMsg string `json:"close_msg"` // close_type
+ UserID string `json:"-"` // user_id
}
type CloseQuestionMeta struct {
@@ -21,6 +31,12 @@ type CloseQuestionMeta struct {
CloseMsg string `json:"close_msg"`
}
+// ReopenQuestionReq reopen question request
+type ReopenQuestionReq struct {
+ QuestionID string `json:"question_id"`
+ UserID string `json:"-"`
+}
+
type QuestionAdd struct {
// question title
Title string `validate:"required,gte=6,lte=150" json:"title"`
@@ -35,6 +51,16 @@ type QuestionAdd struct {
QuestionPermission
}
+func (req *QuestionAdd) Check() (errFields []*validator.FormErrorField, err error) {
+ req.HTML = converter.Markdown2HTML(req.Content)
+ for _, tag := range req.Tags {
+ if len(tag.OriginalText) > 0 {
+ tag.ParsedText = converter.Markdown2HTML(tag.OriginalText)
+ }
+ }
+ return nil, nil
+}
+
type QuestionPermission struct {
// whether user can add it
CanAdd bool `json:"-"`
@@ -44,6 +70,10 @@ type QuestionPermission struct {
CanDelete bool `json:"-"`
// whether user can close it
CanClose bool `json:"-"`
+ // whether user can reopen it
+ CanReopen bool `json:"-"`
+ // whether user can use reserved it
+ CanUseReservedTag bool `json:"-"`
}
type CheckCanQuestionUpdate struct {
@@ -69,11 +99,15 @@ type QuestionUpdate struct {
EditSummary string `validate:"omitempty" json:"edit_summary"`
// user id
UserID string `json:"-"`
- IsAdmin bool `json:"-"`
NoNeedReview bool `json:"-"`
QuestionPermission
}
+func (req *QuestionUpdate) Check() (errFields []*validator.FormErrorField, err error) {
+ req.HTML = converter.Markdown2HTML(req.Content)
+ return nil, nil
+}
+
type QuestionBaseInfo struct {
ID string `json:"id" `
Title string `json:"title" xorm:"title"` // title
@@ -88,8 +122,10 @@ type QuestionBaseInfo struct {
type QuestionInfo struct {
ID string `json:"id" `
Title string `json:"title" xorm:"title"` // title
+ UrlTitle string `json:"url_title" xorm:"url_title"` // title
Content string `json:"content" xorm:"content"` // content
HTML string `json:"html" xorm:"html"` // html
+ Description string `json:"description"` //description
Tags []*TagResp `json:"tags" ` // tags
ViewCount int `json:"view_count" xorm:"view_count"` // view_count
UniqueViewCount int `json:"unique_view_count" xorm:"unique_view_count"` // unique_view_count
@@ -209,3 +245,18 @@ type AdminSetQuestionStatusRequest struct {
StatusStr string `json:"status" form:"status"`
QuestionID string `json:"question_id" form:"question_id"`
}
+
+type SiteMapList struct {
+ QuestionIDs []*SiteMapQuestionInfo `json:"question_ids"`
+ MaxPageNum []int `json:"max_page_num"`
+}
+
+type SiteMapPageList struct {
+ PageData []*SiteMapQuestionInfo `json:"page_data"`
+}
+
+type SiteMapQuestionInfo struct {
+ ID string `json:"id"`
+ Title string `json:"title"`
+ UpdateTime string `json:"time"`
+}
diff --git a/internal/schema/role_schema.go b/internal/schema/role_schema.go
new file mode 100644
index 00000000..6c0d6100
--- /dev/null
+++ b/internal/schema/role_schema.go
@@ -0,0 +1,8 @@
+package schema
+
+// GetRoleResp get role response
+type GetRoleResp struct {
+ ID int `json:"id"`
+ Name string `json:"name"`
+ Description string `json:"description"`
+}
diff --git a/internal/schema/siteinfo_schema.go b/internal/schema/siteinfo_schema.go
index d1292623..f13b1c83 100644
--- a/internal/schema/siteinfo_schema.go
+++ b/internal/schema/siteinfo_schema.go
@@ -1,10 +1,17 @@
package schema
import (
+ "context"
"fmt"
"net/url"
+
+ "github.com/answerdev/answer/internal/base/handler"
+ "github.com/answerdev/answer/internal/base/translator"
)
+const PermaLinkQuestionIDAndTitle = 1
+const PermaLinkQuestionID = 2
+
// SiteGeneralReq site general request
type SiteGeneralReq struct {
Name string `validate:"required,gt=1,lte=128" form:"name" json:"name"`
@@ -14,6 +21,11 @@ type SiteGeneralReq struct {
ContactEmail string `validate:"required,gt=1,lte=512,email" form:"contact_email" json:"contact_email"`
}
+type SiteSeoReq struct {
+ PermaLink int `validate:"required,lte=3,gte=0" form:"permalink" json:"permalink"`
+ Robots string `validate:"required" form:"robots" json:"robots"`
+}
+
func (r *SiteGeneralReq) FormatSiteUrl() {
parsedUrl, err := url.Parse(r.SiteUrl)
if err != nil {
@@ -74,6 +86,26 @@ type GetSiteLegalInfoResp struct {
PrivacyPolicyParsedText string `json:"privacy_policy_parsed_text,omitempty"`
}
+// SiteLoginReq site login request
+type SiteLoginReq struct {
+ AllowNewRegistrations bool `json:"allow_new_registrations"`
+ LoginRequired bool `json:"login_required"`
+}
+
+// SiteCustomCssHTMLReq site custom css html
+type SiteCustomCssHTMLReq struct {
+ CustomHead string `validate:"omitempty,gt=0,lte=65536" json:"custom_head"`
+ CustomCss string `validate:"omitempty,gt=0,lte=65536" json:"custom_css"`
+ CustomHeader string `validate:"omitempty,gt=0,lte=65536" json:"custom_header"`
+ CustomFooter string `validate:"omitempty,gt=0,lte=65536" json:"custom_footer"`
+}
+
+// SiteThemeReq site theme config
+type SiteThemeReq struct {
+ Theme string `validate:"required,gt=0,lte=255" json:"theme"`
+ ThemeConfig map[string]interface{} `validate:"omitempty" json:"theme_config"`
+}
+
// SiteGeneralResp site general response
type SiteGeneralResp SiteGeneralReq
@@ -83,17 +115,67 @@ type SiteInterfaceResp SiteInterfaceReq
// SiteBrandingResp site branding response
type SiteBrandingResp SiteBrandingReq
+// SiteLoginResp site login response
+type SiteLoginResp SiteLoginReq
+
+// SiteCustomCssHTMLResp site custom css html response
+type SiteCustomCssHTMLResp SiteCustomCssHTMLReq
+
+// SiteThemeResp site theme response
+type SiteThemeResp struct {
+ ThemeOptions []*ThemeOption `json:"theme_options"`
+ Theme string `json:"theme"`
+ ThemeConfig map[string]interface{} `json:"theme_config"`
+}
+
+func (s *SiteThemeResp) TrTheme(ctx context.Context) {
+ la := handler.GetLangByCtx(ctx)
+ for _, option := range s.ThemeOptions {
+ tr := translator.GlobalTrans.Tr(la, option.Value)
+ // if tr is equal the option value means not found translation, so use the original label
+ if tr != option.Value {
+ option.Label = tr
+ }
+ }
+}
+
+// ThemeOption get label option
+type ThemeOption struct {
+ Label string `json:"label"`
+ Value string `json:"value"`
+}
+
// SiteWriteResp site write response
type SiteWriteResp SiteWriteReq
// SiteLegalResp site write response
type SiteLegalResp SiteLegalReq
+// SiteSeoResp site write response
+type SiteSeoResp SiteSeoReq
+
// SiteInfoResp get site info response
type SiteInfoResp struct {
- General *SiteGeneralResp `json:"general"`
- Interface *SiteInterfaceResp `json:"interface"`
- Branding *SiteBrandingResp `json:"branding"`
+ General *SiteGeneralResp `json:"general"`
+ Interface *SiteInterfaceResp `json:"interface"`
+ Branding *SiteBrandingResp `json:"branding"`
+ Login *SiteLoginResp `json:"login"`
+ Theme *SiteThemeResp `json:"theme"`
+ CustomCssHtml *SiteCustomCssHTMLResp `json:"custom_css_html"`
+ SiteSeo *SiteSeoReq `json:"site_seo"`
+}
+type TemplateSiteInfoResp struct {
+ General *SiteGeneralResp `json:"general"`
+ Interface *SiteInterfaceResp `json:"interface"`
+ Branding *SiteBrandingResp `json:"branding"`
+ SiteSeo *SiteSeoReq `json:"site_seo"`
+ CustomCssHtml *SiteCustomCssHTMLResp `json:"custom_css_html"`
+ Title string
+ Year string
+ Canonical string
+ JsonLD string
+ Keywords string
+ Description string
}
// UpdateSMTPConfigReq get smtp config request
@@ -120,3 +202,16 @@ type GetSMTPConfigResp struct {
SMTPPassword string `json:"smtp_password"`
SMTPAuthentication bool `json:"smtp_authentication"`
}
+
+// GetManifestJsonResp get manifest json response
+type GetManifestJsonResp struct {
+ ManifestVersion int `json:"manifest_version"`
+ Version string `json:"version"`
+ ShortName string `json:"short_name"`
+ Name string `json:"name"`
+ Icons map[string]string `json:"icons"`
+ StartUrl string `json:"start_url"`
+ Display string `json:"display"`
+ ThemeColor string `json:"theme_color"`
+ BackgroundColor string `json:"background_color"`
+}
diff --git a/internal/schema/tag_schema.go b/internal/schema/tag_schema.go
index bf79d5aa..3b7100a2 100644
--- a/internal/schema/tag_schema.go
+++ b/internal/schema/tag_schema.go
@@ -5,6 +5,7 @@ import (
"github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/internal/base/validator"
+ "github.com/answerdev/answer/pkg/converter"
"github.com/segmentfault/pacman/errors"
)
@@ -29,6 +30,17 @@ type GetTagInfoReq struct {
CanDelete bool `json:"-"`
}
+type GetTamplateTagInfoReq struct {
+ // tag id
+ ID string `validate:"omitempty" form:"id"`
+ // tag slug name
+ Name string `validate:"omitempty" form:"name"`
+ // user id
+ UserID string `json:"-"`
+ Page int `validate:"omitempty,min=1" form:"page"`
+ PageSize int `validate:"omitempty,min=1" form:"page_size"`
+}
+
func (r *GetTagInfoReq) Check() (errFields []*validator.FormErrorField, err error) {
if len(r.ID) == 0 && len(r.Name) == 0 {
return nil, errors.BadRequest(reason.RequestFormatError)
@@ -55,6 +67,8 @@ type GetTagResp struct {
OriginalText string `json:"original_text"`
// parsed text
ParsedText string `json:"parsed_text"`
+ // description text
+ Description string `json:"description"`
// follower amount
FollowCount int `json:"follow_count"`
// question amount
@@ -162,8 +176,9 @@ type UpdateTagReq struct {
func (r *UpdateTagReq) Check() (errFields []*validator.FormErrorField, err error) {
if len(r.EditSummary) == 0 {
- r.EditSummary = "tag.edit.summary" // to do i18n
+ r.EditSummary = "tag.edit.summary"
}
+ r.ParsedText = converter.Markdown2HTML(r.OriginalText)
return nil, nil
}
diff --git a/internal/schema/template_schema.go b/internal/schema/template_schema.go
new file mode 100644
index 00000000..7273f960
--- /dev/null
+++ b/internal/schema/template_schema.go
@@ -0,0 +1,54 @@
+package schema
+
+import "time"
+
+type Paginator struct {
+ Pages []int
+ Totalpages int
+ Prevpage int
+ Nextpage int
+ Currpage int
+}
+
+type QAPageJsonLD struct {
+ Context string `json:"@context"`
+ Type string `json:"@type"`
+ MainEntity struct {
+ Type string `json:"@type"`
+ Name string `json:"name"`
+ Text string `json:"text"`
+ AnswerCount int `json:"answerCount"`
+ UpvoteCount int `json:"upvoteCount"`
+ DateCreated time.Time `json:"dateCreated"`
+ Author struct {
+ Type string `json:"@type"`
+ Name string `json:"name"`
+ } `json:"author"`
+ AcceptedAnswer *AcceptedAnswerItem `json:"acceptedAnswer,omitempty"`
+ SuggestedAnswer []*SuggestedAnswerItem `json:"suggestedAnswer"`
+ } `json:"mainEntity"`
+}
+
+type AcceptedAnswerItem struct {
+ Type string `json:"@type"`
+ Text string `json:"text"`
+ DateCreated time.Time `json:"dateCreated"`
+ UpvoteCount int `json:"upvoteCount"`
+ URL string `json:"url"`
+ Author struct {
+ Type string `json:"@type"`
+ Name string `json:"name"`
+ } `json:"author"`
+}
+
+type SuggestedAnswerItem struct {
+ Type string `json:"@type"`
+ Text string `json:"text"`
+ DateCreated time.Time `json:"dateCreated"`
+ UpvoteCount int `json:"upvoteCount"`
+ URL string `json:"url"`
+ Author struct {
+ Type string `json:"@type"`
+ Name string `json:"name"`
+ } `json:"author"`
+}
diff --git a/internal/schema/theme_schema.go b/internal/schema/theme_schema.go
index d5eb49b6..da1a79ae 100644
--- a/internal/schema/theme_schema.go
+++ b/internal/schema/theme_schema.go
@@ -1,22 +1,8 @@
package schema
-// GetThemeOption get label option
-type GetThemeOption struct {
- Label string `json:"label"`
- Value string `json:"value"`
-}
-
-var GetThemeOptions = []*GetThemeOption{
+var GetThemeOptions = []*ThemeOption{
{
Label: "Default",
Value: "default",
},
- {
- Label: "Black",
- Value: "black",
- },
- {
- Label: "White",
- Value: "white",
- },
}
diff --git a/internal/schema/user_schema.go b/internal/schema/user_schema.go
index dd36f8c2..520cb2a8 100644
--- a/internal/schema/user_schema.go
+++ b/internal/schema/user_schema.go
@@ -106,16 +106,16 @@ func FormatAvatarInfo(avatarJson string) string {
if avatarJson == "" {
return ""
}
- AvatarInfo := &AvatarInfo{}
- err := json.Unmarshal([]byte(avatarJson), AvatarInfo)
+ avatarInfo := &AvatarInfo{}
+ err := json.Unmarshal([]byte(avatarJson), avatarInfo)
if err != nil {
return ""
}
- switch AvatarInfo.Type {
+ switch avatarInfo.Type {
case "gravatar":
- return AvatarInfo.Gravatar
+ return avatarInfo.Gravatar
case "custom":
- return AvatarInfo.Custom
+ return avatarInfo.Custom
default:
return ""
}
@@ -232,8 +232,10 @@ type UserRegisterReq struct {
// email
Email string `validate:"required,email,gt=0,lte=500" json:"e_mail" `
// password
- Pass string `validate:"required,gte=8,lte=32" json:"pass"`
- IP string `json:"-" `
+ Pass string `validate:"required,gte=8,lte=32" json:"pass"`
+ IP string `json:"-" `
+ CaptchaID string `json:"captcha_id"` // captcha_id
+ CaptchaCode string `json:"captcha_code"` // captcha_code
}
func (u *UserRegisterReq) Check() (errFields []*validator.FormErrorField, err error) {
@@ -368,8 +370,7 @@ type ActionRecordResp struct {
}
type UserBasicInfo struct {
- ID string `json:"-"` // user_id
- IsAdmin bool `json:"-"`
+ ID string `json:"id"` // user_id
Username string `json:"username" ` // name
Rank int `json:"rank" ` // rank
DisplayName string `json:"display_name"` // display_name
@@ -418,3 +419,24 @@ type UserVerifyEmailSendReq struct {
CaptchaID string `validate:"omitempty,gt=0,lte=500" json:"captcha_id"`
CaptchaCode string `validate:"omitempty,gt=0,lte=500" json:"captcha_code"`
}
+
+// UserRankingResp user ranking response
+type UserRankingResp struct {
+ UsersWithTheMostReputation []*UserRankingSimpleInfo `json:"users_with_the_most_reputation"`
+ UsersWithTheMostVote []*UserRankingSimpleInfo `json:"users_with_the_most_vote"`
+ Staffs []*UserRankingSimpleInfo `json:"staffs"`
+}
+
+// UserRankingSimpleInfo user ranking simple info
+type UserRankingSimpleInfo struct {
+ // username
+ Username string `json:"username"`
+ // rank
+ Rank int `json:"rank"`
+ // vote
+ VoteCount int `json:"vote_count"`
+ // display name
+ DisplayName string `json:"display_name"`
+ // avatar
+ Avatar string `json:"avatar"`
+}
diff --git a/internal/service/action/captcha_service.go b/internal/service/action/captcha_service.go
index 9d79c0fe..a6ee8e04 100644
--- a/internal/service/action/captcha_service.go
+++ b/internal/service/action/captcha_service.go
@@ -48,6 +48,26 @@ func (cs *CaptchaService) ActionRecord(ctx context.Context, req *schema.ActionRe
return
}
+func (cs *CaptchaService) UserRegisterCaptcha(ctx context.Context) (resp *schema.ActionRecordResp, err error) {
+ resp = &schema.ActionRecordResp{}
+ resp.CaptchaID, resp.CaptchaImg, err = cs.GenerateCaptcha(ctx)
+ resp.Verify = true
+ return
+}
+
+func (cs *CaptchaService) UserRegisterVerifyCaptcha(
+ ctx context.Context, id string, VerifyValue string,
+) bool {
+ if id == "" || VerifyValue == "" {
+ return false
+ }
+ pass, err := cs.VerifyCaptcha(ctx, id, VerifyValue)
+ if err != nil {
+ return false
+ }
+ return pass
+}
+
// ActionRecordVerifyCaptcha
// Verify that you need to enter a CAPTCHA, and that the CAPTCHA is correct
func (cs *CaptchaService) ActionRecordVerifyCaptcha(
@@ -58,6 +78,9 @@ func (cs *CaptchaService) ActionRecordVerifyCaptcha(
return true
}
if num >= 3 {
+ if id == "" || VerifyValue == "" {
+ return false
+ }
pass, err := cs.VerifyCaptcha(ctx, id, VerifyValue)
if err != nil {
return false
diff --git a/internal/service/activity_common/activity.go b/internal/service/activity_common/activity.go
index 8d76ac2c..d19a91f7 100644
--- a/internal/service/activity_common/activity.go
+++ b/internal/service/activity_common/activity.go
@@ -2,6 +2,7 @@ package activity_common
import (
"context"
+ "time"
"github.com/answerdev/answer/internal/entity"
"github.com/answerdev/answer/internal/service/activity_queue"
@@ -18,6 +19,10 @@ type ActivityRepo interface {
GetUserIDObjectIDActivitySum(ctx context.Context, userID, objectID string) (int, error)
GetActivityTypeByConfigKey(ctx context.Context, configKey string) (activityType int, err error)
AddActivity(ctx context.Context, activity *entity.Activity) (err error)
+ GetUsersWhoHasGainedTheMostReputation(
+ ctx context.Context, startTime, endTime time.Time, limit int) (rankStat []*entity.ActivityUserRankStat, err error)
+ GetUsersWhoHasVoteMost(
+ ctx context.Context, startTime, endTime time.Time, limit int) (voteStat []*entity.ActivityUserVoteStat, err error)
}
type ActivityCommon struct {
diff --git a/internal/service/activity_type/activity_type.go b/internal/service/activity_type/activity_type.go
index dde03890..3b86799e 100644
--- a/internal/service/activity_type/activity_type.go
+++ b/internal/service/activity_type/activity_type.go
@@ -12,6 +12,14 @@ const (
)
var (
+ ActivityTypeList = []string{
+ QuestionVoteUp,
+ QuestionVoteDown,
+ AnswerVoteUp,
+ AnswerVoteDown,
+ CommentVoteUp,
+ CommentVoteDown,
+ }
activityTypeFlagMapping = map[string]string{
QuestionVoteUp: "upvote",
QuestionVoteDown: "downvote",
diff --git a/internal/service/auth/auth.go b/internal/service/auth/auth.go
index 1f77d174..9f3bbb75 100644
--- a/internal/service/auth/auth.go
+++ b/internal/service/auth/auth.go
@@ -19,6 +19,8 @@ type AuthRepo interface {
GetBackyardUserCacheInfo(ctx context.Context, accessToken string) (userInfo *entity.UserCacheInfo, err error)
SetBackyardUserCacheInfo(ctx context.Context, accessToken string, userInfo *entity.UserCacheInfo) error
RemoveBackyardUserCacheInfo(ctx context.Context, accessToken string) (err error)
+ AddUserTokenMapping(ctx context.Context, userID, accessToken string) (err error)
+ RemoveAllUserTokens(ctx context.Context, userID string)
}
// AuthService kit service
@@ -78,6 +80,16 @@ func (as *AuthService) RemoveUserCacheInfo(ctx context.Context, accessToken stri
return as.authRepo.RemoveUserCacheInfo(ctx, accessToken)
}
+// AddUserTokenMapping add user token mapping
+func (as *AuthService) AddUserTokenMapping(ctx context.Context, userID, accessToken string) (err error) {
+ return as.authRepo.AddUserTokenMapping(ctx, userID, accessToken)
+}
+
+// RemoveAllUserTokens Log out all users under this user id
+func (as *AuthService) RemoveAllUserTokens(ctx context.Context, userID string) {
+ as.authRepo.RemoveAllUserTokens(ctx, userID)
+}
+
//cms
func (as *AuthService) GetCmsUserCacheInfo(ctx context.Context, accessToken string) (userInfo *entity.UserCacheInfo, err error) {
diff --git a/internal/service/dashboard/dashboard_service.go b/internal/service/dashboard/dashboard_service.go
index 8495ab0a..11868acd 100644
--- a/internal/service/dashboard/dashboard_service.go
+++ b/internal/service/dashboard/dashboard_service.go
@@ -4,7 +4,7 @@ import (
"context"
"encoding/json"
"fmt"
- "io/ioutil"
+ "io"
"net/http"
"net/url"
"time"
@@ -208,7 +208,7 @@ func (ds *DashboardService) RemoteVersion(ctx context.Context) string {
}
defer resp.Body.Close()
- respByte, err := ioutil.ReadAll(resp.Body)
+ respByte, err := io.ReadAll(resp.Body)
if err != nil {
log.Error("http.Client error", err)
return ""
diff --git a/internal/service/export/email_service.go b/internal/service/export/email_service.go
index 42058543..9f078224 100644
--- a/internal/service/export/email_service.go
+++ b/internal/service/export/email_service.go
@@ -122,7 +122,7 @@ func (es *EmailService) Send(ctx context.Context, toEmailAddr, subject, body, co
func (es *EmailService) VerifyUrlExpired(ctx context.Context, code string) (content string) {
content, err := es.emailRepo.VerifyCode(ctx, code)
if err != nil {
- log.Error(err)
+ log.Warn(err)
}
return content
}
@@ -277,6 +277,9 @@ func (es *EmailService) TestTemplate(ctx context.Context) (title, body string, e
return "", "", fmt.Errorf("email test body template parse error: %s", err)
}
tmpl, err = template.New("test_body").Parse(ec.TestBody)
+ if err != nil {
+ return "", "", fmt.Errorf("test_body template parse error: %s", err)
+ }
err = tmpl.Execute(bodyBuf, templateData)
if err != nil {
return "", "", err
diff --git a/internal/service/mock/siteinfo_repo_mock.go b/internal/service/mock/siteinfo_repo_mock.go
new file mode 100644
index 00000000..772d36fe
--- /dev/null
+++ b/internal/service/mock/siteinfo_repo_mock.go
@@ -0,0 +1,66 @@
+// Code generated by MockGen. DO NOT EDIT.
+// Source: ./siteinfo_service.go
+
+// Package mock is a generated GoMock package.
+package mock
+
+import (
+ context "context"
+ reflect "reflect"
+
+ entity "github.com/answerdev/answer/internal/entity"
+ gomock "github.com/golang/mock/gomock"
+)
+
+// MockSiteInfoRepo is a mock of SiteInfoRepo interface.
+type MockSiteInfoRepo struct {
+ ctrl *gomock.Controller
+ recorder *MockSiteInfoRepoMockRecorder
+}
+
+// MockSiteInfoRepoMockRecorder is the mock recorder for MockSiteInfoRepo.
+type MockSiteInfoRepoMockRecorder struct {
+ mock *MockSiteInfoRepo
+}
+
+// NewMockSiteInfoRepo creates a new mock instance.
+func NewMockSiteInfoRepo(ctrl *gomock.Controller) *MockSiteInfoRepo {
+ mock := &MockSiteInfoRepo{ctrl: ctrl}
+ mock.recorder = &MockSiteInfoRepoMockRecorder{mock}
+ return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *MockSiteInfoRepo) EXPECT() *MockSiteInfoRepoMockRecorder {
+ return m.recorder
+}
+
+// GetByType mocks base method.
+func (m *MockSiteInfoRepo) GetByType(ctx context.Context, siteType string) (*entity.SiteInfo, bool, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "GetByType", ctx, siteType)
+ ret0, _ := ret[0].(*entity.SiteInfo)
+ ret1, _ := ret[1].(bool)
+ ret2, _ := ret[2].(error)
+ return ret0, ret1, ret2
+}
+
+// GetByType indicates an expected call of GetByType.
+func (mr *MockSiteInfoRepoMockRecorder) GetByType(ctx, siteType interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetByType", reflect.TypeOf((*MockSiteInfoRepo)(nil).GetByType), ctx, siteType)
+}
+
+// SaveByType mocks base method.
+func (m *MockSiteInfoRepo) SaveByType(ctx context.Context, siteType string, data *entity.SiteInfo) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "SaveByType", ctx, siteType, data)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// SaveByType indicates an expected call of SaveByType.
+func (mr *MockSiteInfoRepoMockRecorder) SaveByType(ctx, siteType, data interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveByType", reflect.TypeOf((*MockSiteInfoRepo)(nil).SaveByType), ctx, siteType, data)
+}
diff --git a/internal/service/object_info/object_info.go b/internal/service/object_info/object_info.go
index 9ab18e70..ab961d26 100644
--- a/internal/service/object_info/object_info.go
+++ b/internal/service/object_info/object_info.go
@@ -40,8 +40,6 @@ func NewObjService(
}
}
func (os *ObjService) GetUnreviewedRevisionInfo(ctx context.Context, objectID string) (objInfo *schema.UnreviewedRevisionInfoInfo, err error) {
- objInfo = &schema.UnreviewedRevisionInfoInfo{}
-
objectType, err := obj.GetObjectTypeStrByObjectID(objectID)
if err != nil {
return nil, err
@@ -150,6 +148,9 @@ func (os *ObjService) GetInfo(ctx context.Context, objectID string) (objInfo *sc
if err != nil {
return nil, err
}
+ if !exist {
+ break
+ }
objInfo = &schema.SimpleObjectInfo{
ObjectID: answerInfo.ID,
ObjectCreatorUserID: answerInfo.UserID,
diff --git a/internal/service/permission/answer_permission.go b/internal/service/permission/answer_permission.go
new file mode 100644
index 00000000..57913c99
--- /dev/null
+++ b/internal/service/permission/answer_permission.go
@@ -0,0 +1,36 @@
+package permission
+
+import (
+ "context"
+
+ "github.com/answerdev/answer/internal/schema"
+)
+
+// GetAnswerPermission get answer permission
+func GetAnswerPermission(ctx context.Context, userID string, creatorUserID string, canEdit, canDelete bool) (
+ actions []*schema.PermissionMemberAction) {
+ actions = make([]*schema.PermissionMemberAction, 0)
+ if len(userID) > 0 {
+ actions = append(actions, &schema.PermissionMemberAction{
+ Action: "report",
+ Name: "Flag",
+ Type: "reason",
+ })
+ }
+ if canEdit || userID == creatorUserID {
+ actions = append(actions, &schema.PermissionMemberAction{
+ Action: "edit",
+ Name: "Edit",
+ Type: "edit",
+ })
+ }
+
+ if canDelete || userID == creatorUserID {
+ actions = append(actions, &schema.PermissionMemberAction{
+ Action: "delete",
+ Name: "Delete",
+ Type: "confirm",
+ })
+ }
+ return actions
+}
diff --git a/internal/service/permission/comment_permission.go b/internal/service/permission/comment_permission.go
index 663bb5b3..026b885a 100644
--- a/internal/service/permission/comment_permission.go
+++ b/internal/service/permission/comment_permission.go
@@ -34,103 +34,3 @@ func GetCommentPermission(ctx context.Context, userID string, creatorUserID stri
}
return actions
}
-
-// GetTagPermission get tag permission
-func GetTagPermission(ctx context.Context, canEdit, canDelete bool) (
- actions []*schema.PermissionMemberAction) {
- actions = make([]*schema.PermissionMemberAction, 0)
- if canEdit {
- actions = append(actions, &schema.PermissionMemberAction{
- Action: "edit",
- Name: "Edit",
- Type: "edit",
- })
- }
-
- if canDelete {
- actions = append(actions, &schema.PermissionMemberAction{
- Action: "delete",
- Name: "Delete",
- Type: "reason",
- })
- }
- return actions
-}
-
-// GetAnswerPermission get answer permission
-func GetAnswerPermission(ctx context.Context, userID string, creatorUserID string, canEdit, canDelete bool) (
- actions []*schema.PermissionMemberAction) {
- actions = make([]*schema.PermissionMemberAction, 0)
- if len(userID) > 0 {
- actions = append(actions, &schema.PermissionMemberAction{
- Action: "report",
- Name: "Flag",
- Type: "reason",
- })
- }
- if canEdit || userID == creatorUserID {
- actions = append(actions, &schema.PermissionMemberAction{
- Action: "edit",
- Name: "Edit",
- Type: "edit",
- })
- }
-
- if canDelete || userID == creatorUserID {
- actions = append(actions, &schema.PermissionMemberAction{
- Action: "delete",
- Name: "Delete",
- Type: "confirm",
- })
- }
- return actions
-}
-
-// GetQuestionPermission get question permission
-func GetQuestionPermission(ctx context.Context, userID string, creatorUserID string, canEdit, canDelete, canClose bool) (
- actions []*schema.PermissionMemberAction) {
- actions = make([]*schema.PermissionMemberAction, 0)
- if len(userID) > 0 {
- actions = append(actions, &schema.PermissionMemberAction{
- Action: "report",
- Name: "Flag",
- Type: "reason",
- })
- }
- if canEdit || userID == creatorUserID {
- actions = append(actions, &schema.PermissionMemberAction{
- Action: "edit",
- Name: "Edit",
- Type: "edit",
- })
- }
- if canClose {
- actions = append(actions, &schema.PermissionMemberAction{
- Action: "close",
- Name: "Close",
- Type: "confirm",
- })
- }
- if canDelete || userID == creatorUserID {
- actions = append(actions, &schema.PermissionMemberAction{
- Action: "delete",
- Name: "Delete",
- Type: "confirm",
- })
- }
- return actions
-}
-
-// GetTagSynonymPermission get tag synonym permission
-func GetTagSynonymPermission(ctx context.Context, canEdit bool) (
- actions []*schema.PermissionMemberAction) {
- actions = make([]*schema.PermissionMemberAction, 0)
- if canEdit {
- actions = append(actions, &schema.PermissionMemberAction{
- Action: "edit",
- Name: "Edit",
- Type: "edit",
- })
- }
- return actions
-}
diff --git a/internal/service/permission/permission_name.go b/internal/service/permission/permission_name.go
new file mode 100644
index 00000000..45c91278
--- /dev/null
+++ b/internal/service/permission/permission_name.go
@@ -0,0 +1,38 @@
+package permission
+
+const (
+ AdminAccess = "admin.access"
+ QuestionAdd = "question.add"
+ QuestionEdit = "question.edit"
+ QuestionEditWithoutReview = "question.edit_without_review"
+ QuestionDelete = "question.delete"
+ QuestionClose = "question.close"
+ QuestionReopen = "question.reopen"
+ QuestionVoteUp = "question.vote_up"
+ QuestionVoteDown = "question.vote_down"
+ AnswerAdd = "answer.add"
+ AnswerEdit = "answer.edit"
+ AnswerEditWithoutReview = "answer.edit_without_review"
+ AnswerDelete = "answer.delete"
+ AnswerAccept = "answer.accept"
+ AnswerVoteUp = "answer.vote_up"
+ AnswerVoteDown = "answer.vote_down"
+ CommentAdd = "comment.add"
+ CommentEdit = "comment.edit"
+ CommentDelete = "comment.delete"
+ CommentVoteUp = "comment.vote_up"
+ CommentVoteDown = "comment.vote_down"
+ ReportAdd = "report.add"
+ TagAdd = "tag.add"
+ TagEdit = "tag.edit"
+ TagEditSlugName = "tag.edit_slug_name"
+ TagEditWithoutReview = "tag.edit_without_review"
+ TagDelete = "tag.delete"
+ TagSynonym = "tag.synonym"
+ LinkUrlLimit = "link.url_limit"
+ VoteDetail = "vote.detail"
+ AnswerAudit = "answer.audit"
+ QuestionAudit = "question.audit"
+ TagAudit = "tag.audit"
+ TagUseReservedTag = "tag.use_reserved_tag"
+)
diff --git a/internal/service/permission/question_permission.go b/internal/service/permission/question_permission.go
new file mode 100644
index 00000000..4d1e1a42
--- /dev/null
+++ b/internal/service/permission/question_permission.go
@@ -0,0 +1,50 @@
+package permission
+
+import (
+ "context"
+
+ "github.com/answerdev/answer/internal/schema"
+)
+
+// GetQuestionPermission get question permission
+func GetQuestionPermission(ctx context.Context, userID string, creatorUserID string,
+ canEdit, canDelete, canClose, canReopen bool) (
+ actions []*schema.PermissionMemberAction) {
+ actions = make([]*schema.PermissionMemberAction, 0)
+ if len(userID) > 0 {
+ actions = append(actions, &schema.PermissionMemberAction{
+ Action: "report",
+ Name: "Flag",
+ Type: "reason",
+ })
+ }
+ if canEdit || userID == creatorUserID {
+ actions = append(actions, &schema.PermissionMemberAction{
+ Action: "edit",
+ Name: "Edit",
+ Type: "edit",
+ })
+ }
+ if canClose {
+ actions = append(actions, &schema.PermissionMemberAction{
+ Action: "close",
+ Name: "Close",
+ Type: "confirm",
+ })
+ }
+ if canReopen {
+ actions = append(actions, &schema.PermissionMemberAction{
+ Action: "reopen",
+ Name: "Reopen",
+ Type: "confirm",
+ })
+ }
+ if canDelete || userID == creatorUserID {
+ actions = append(actions, &schema.PermissionMemberAction{
+ Action: "delete",
+ Name: "Delete",
+ Type: "confirm",
+ })
+ }
+ return actions
+}
diff --git a/internal/service/permission/tag_permission.go b/internal/service/permission/tag_permission.go
new file mode 100644
index 00000000..b4b156b4
--- /dev/null
+++ b/internal/service/permission/tag_permission.go
@@ -0,0 +1,43 @@
+package permission
+
+import (
+ "context"
+
+ "github.com/answerdev/answer/internal/schema"
+)
+
+// GetTagPermission get tag permission
+func GetTagPermission(ctx context.Context, canEdit, canDelete bool) (
+ actions []*schema.PermissionMemberAction) {
+ actions = make([]*schema.PermissionMemberAction, 0)
+ if canEdit {
+ actions = append(actions, &schema.PermissionMemberAction{
+ Action: "edit",
+ Name: "Edit",
+ Type: "edit",
+ })
+ }
+
+ if canDelete {
+ actions = append(actions, &schema.PermissionMemberAction{
+ Action: "delete",
+ Name: "Delete",
+ Type: "reason",
+ })
+ }
+ return actions
+}
+
+// GetTagSynonymPermission get tag synonym permission
+func GetTagSynonymPermission(ctx context.Context, canEdit bool) (
+ actions []*schema.PermissionMemberAction) {
+ actions = make([]*schema.PermissionMemberAction, 0)
+ if canEdit {
+ actions = append(actions, &schema.PermissionMemberAction{
+ Action: "edit",
+ Name: "Edit",
+ Type: "edit",
+ })
+ }
+ return actions
+}
diff --git a/internal/service/provider.go b/internal/service/provider.go
index 6d1342d9..44a8f853 100644
--- a/internal/service/provider.go
+++ b/internal/service/provider.go
@@ -23,6 +23,7 @@ import (
"github.com/answerdev/answer/internal/service/report_backyard"
"github.com/answerdev/answer/internal/service/report_handle_backyard"
"github.com/answerdev/answer/internal/service/revision_common"
+ "github.com/answerdev/answer/internal/service/role"
"github.com/answerdev/answer/internal/service/search_parser"
"github.com/answerdev/answer/internal/service/siteinfo"
"github.com/answerdev/answer/internal/service/siteinfo_common"
@@ -75,4 +76,7 @@ var ProviderSetService = wire.NewSet(
dashboard.NewDashboardService,
activity_common.NewActivityCommon,
activity.NewActivityService,
+ role.NewRoleService,
+ role.NewUserRoleRelService,
+ role.NewRolePowerRelService,
)
diff --git a/internal/service/question_common/question.go b/internal/service/question_common/question.go
index 3be2c3f9..f606e038 100644
--- a/internal/service/question_common/question.go
+++ b/internal/service/question_common/question.go
@@ -11,6 +11,7 @@ import (
"github.com/answerdev/answer/internal/service/activity_queue"
"github.com/answerdev/answer/internal/service/config"
"github.com/answerdev/answer/internal/service/meta"
+ "github.com/answerdev/answer/pkg/htmltext"
"github.com/segmentfault/pacman/errors"
"github.com/answerdev/answer/internal/entity"
@@ -41,6 +42,7 @@ type QuestionRepo interface {
FindByID(ctx context.Context, id []string) (questionList []*entity.Question, err error)
CmsSearchList(ctx context.Context, search *schema.CmsQuestionSearch) ([]*entity.Question, int64, error)
GetQuestionCount(ctx context.Context) (count int64, err error)
+ GetQuestionIDsPage(ctx context.Context, page, pageSize int) (questionIDList []*schema.SiteMapQuestionInfo, err error)
}
// QuestionCommon user service
@@ -393,6 +395,7 @@ func (qs *QuestionCommon) ShowFormat(ctx context.Context, data *entity.Question)
info := schema.QuestionInfo{}
info.ID = data.ID
info.Title = data.Title
+ info.UrlTitle = htmltext.UrlTitle(data.Title)
info.Content = data.OriginalText
info.HTML = data.ParsedText
info.ViewCount = data.ViewCount
diff --git a/internal/service/question_service.go b/internal/service/question_service.go
index c0d63dd7..7a4635a0 100644
--- a/internal/service/question_service.go
+++ b/internal/service/question_service.go
@@ -3,10 +3,12 @@ package service
import (
"encoding/json"
"fmt"
+ "math"
"strings"
"time"
"github.com/answerdev/answer/internal/base/constant"
+ "github.com/answerdev/answer/internal/base/data"
"github.com/answerdev/answer/internal/base/handler"
"github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/internal/base/translator"
@@ -23,6 +25,7 @@ import (
"github.com/answerdev/answer/internal/service/revision_common"
tagcommon "github.com/answerdev/answer/internal/service/tag_common"
usercommon "github.com/answerdev/answer/internal/service/user_common"
+ "github.com/answerdev/answer/pkg/htmltext"
"github.com/jinzhu/copier"
"github.com/segmentfault/pacman/errors"
"github.com/segmentfault/pacman/i18n"
@@ -42,6 +45,7 @@ type QuestionService struct {
metaService *meta.MetaService
collectionCommon *collectioncommon.CollectionCommon
answerActivityService *activity.AnswerActivityService
+ data *data.Data
}
func NewQuestionService(
@@ -53,6 +57,8 @@ func NewQuestionService(
metaService *meta.MetaService,
collectionCommon *collectioncommon.CollectionCommon,
answerActivityService *activity.AnswerActivityService,
+ data *data.Data,
+
) *QuestionService {
return &QuestionService{
questionRepo: questionRepo,
@@ -63,6 +69,7 @@ func NewQuestionService(
metaService: metaService,
collectionCommon: collectionCommon,
answerActivityService: answerActivityService,
+ data: data,
}
}
@@ -75,11 +82,6 @@ func (qs *QuestionService) CloseQuestion(ctx context.Context, req *schema.CloseQ
return nil
}
- if !req.IsAdmin {
- if questionInfo.UserID != req.UserID {
- return errors.BadRequest(reason.QuestionCannotClose)
- }
- }
questionInfo.Status = entity.QuestionStatusClosed
err = qs.questionRepo.UpdateQuestionStatus(ctx, questionInfo)
if err != nil {
@@ -104,6 +106,30 @@ func (qs *QuestionService) CloseQuestion(ctx context.Context, req *schema.CloseQ
return nil
}
+// ReopenQuestion reopen question
+func (qs *QuestionService) ReopenQuestion(ctx context.Context, req *schema.ReopenQuestionReq) error {
+ questionInfo, has, err := qs.questionRepo.GetQuestion(ctx, req.QuestionID)
+ if err != nil {
+ return err
+ }
+ if !has {
+ return nil
+ }
+
+ questionInfo.Status = entity.QuestionStatusAvailable
+ err = qs.questionRepo.UpdateQuestionStatus(ctx, questionInfo)
+ if err != nil {
+ return err
+ }
+ activity_queue.AddActivity(&schema.ActivityMsg{
+ UserID: req.UserID,
+ ObjectID: questionInfo.ID,
+ OriginalObjectID: questionInfo.ID,
+ ActivityTypeKey: constant.ActQuestionReopened,
+ })
+ return nil
+}
+
// CloseMsgList list close question condition
func (qs *QuestionService) CloseMsgList(ctx context.Context, lang i18n.Language) (
resp []*schema.GetCloseTypeResp, err error,
@@ -120,6 +146,19 @@ func (qs *QuestionService) CloseMsgList(ctx context.Context, lang i18n.Language)
return resp, err
}
+func (qs *QuestionService) AddQuestionCheckTags(ctx context.Context, Tags []*entity.Tag) ([]string, error) {
+ list := make([]string, 0)
+ for _, tag := range Tags {
+ if tag.Reserved {
+ list = append(list, tag.DisplayName)
+ }
+ }
+ if len(list) > 0 {
+ return list, errors.BadRequest(reason.RequestFormatError)
+ }
+ return []string{}, nil
+}
+
// AddQuestion add question
func (qs *QuestionService) AddQuestion(ctx context.Context, req *schema.QuestionAdd) (questionInfo any, err error) {
recommendExist, err := qs.tagCommon.ExistRecommend(ctx, req.Tags)
@@ -136,6 +175,29 @@ func (qs *QuestionService) AddQuestion(ctx context.Context, req *schema.Question
return errorlist, err
}
+ tagNameList := make([]string, 0)
+ for _, tag := range req.Tags {
+ tagNameList = append(tagNameList, tag.SlugName)
+ }
+ Tags, tagerr := qs.tagCommon.GetTagListByNames(ctx, tagNameList)
+ if tagerr != nil {
+ return questionInfo, tagerr
+ }
+ if !req.QuestionPermission.CanUseReservedTag {
+ taglist, err := qs.AddQuestionCheckTags(ctx, Tags)
+ errMsg := fmt.Sprintf(`"%s" can only be used by moderators.`,
+ strings.Join(taglist, ","))
+ if err != nil {
+ errorlist := make([]*validator.FormErrorField, 0)
+ errorlist = append(errorlist, &validator.FormErrorField{
+ ErrorField: "tags",
+ ErrorMsg: errMsg,
+ })
+ err = errors.BadRequest(reason.RecommendTagEnter)
+ return errorlist, err
+ }
+ }
+
question := &entity.Question{}
now := time.Now()
question.UserID = req.UserID
@@ -169,15 +231,6 @@ func (qs *QuestionService) AddQuestion(ctx context.Context, req *schema.Question
Title: question.Title,
}
- tagNameList := make([]string, 0)
- for _, tag := range req.Tags {
- tagNameList = append(tagNameList, tag.SlugName)
- }
- Tags, tagerr := qs.tagCommon.GetTagListByNames(ctx, tagNameList)
- if tagerr != nil {
- return questionInfo, tagerr
- }
-
questionWithTagsRevision, err := qs.changeQuestionToRevision(ctx, question, Tags)
if err != nil {
return nil, err
@@ -268,6 +321,72 @@ func (qs *QuestionService) RemoveQuestion(ctx context.Context, req *schema.Remov
return nil
}
+func (qs *QuestionService) UpdateQuestionCheckTags(ctx context.Context, req *schema.QuestionUpdate) (errorlist []*validator.FormErrorField, err error) {
+ dbinfo, has, err := qs.questionRepo.GetQuestion(ctx, req.ID)
+ if err != nil {
+ return
+ }
+ if !has {
+ return
+ }
+
+ oldTags, tagerr := qs.tagCommon.GetObjectEntityTag(ctx, req.ID)
+ if tagerr != nil {
+ log.Error("GetObjectEntityTag error", tagerr)
+ return nil, nil
+ }
+
+ tagNameList := make([]string, 0)
+ oldtagNameList := make([]string, 0)
+ for _, tag := range req.Tags {
+ tagNameList = append(tagNameList, tag.SlugName)
+ }
+ for _, tag := range oldTags {
+ oldtagNameList = append(oldtagNameList, tag.SlugName)
+ }
+
+ isChange := qs.tagCommon.CheckTagsIsChange(ctx, tagNameList, oldtagNameList)
+
+ //If the content is the same, ignore it
+ if dbinfo.Title == req.Title && dbinfo.OriginalText == req.Content && !isChange {
+ return
+ }
+
+ Tags, tagerr := qs.tagCommon.GetTagListByNames(ctx, tagNameList)
+ if tagerr != nil {
+ log.Error("GetTagListByNames error", tagerr)
+ return nil, nil
+ }
+
+ // if user can not use reserved tag, old reserved tag can not be removed and new reserved tag can not be added.
+ if !req.CanUseReservedTag {
+ CheckOldTag, CheckNewTag, CheckOldTaglist, CheckNewTaglist := qs.CheckChangeReservedTag(ctx, oldTags, Tags)
+ if !CheckOldTag {
+ errMsg := fmt.Sprintf(`The reserved tag "%s" must be present.`,
+ strings.Join(CheckOldTaglist, ","))
+ errorlist := make([]*validator.FormErrorField, 0)
+ errorlist = append(errorlist, &validator.FormErrorField{
+ ErrorField: "tags",
+ ErrorMsg: errMsg,
+ })
+ err = errors.BadRequest(reason.RequestFormatError).WithMsg(errMsg)
+ return errorlist, err
+ }
+ if !CheckNewTag {
+ errMsg := fmt.Sprintf(`"%s" can only be used by moderators.`,
+ strings.Join(CheckNewTaglist, ","))
+ errorlist := make([]*validator.FormErrorField, 0)
+ errorlist = append(errorlist, &validator.FormErrorField{
+ ErrorField: "tags",
+ ErrorMsg: errMsg,
+ })
+ err = errors.BadRequest(reason.RequestFormatError).WithMsg(errMsg)
+ return errorlist, err
+ }
+ }
+ return nil, nil
+}
+
// UpdateQuestion update question
func (qs *QuestionService) UpdateQuestion(ctx context.Context, req *schema.QuestionUpdate) (questionInfo any, err error) {
var canUpdate bool
@@ -332,14 +451,23 @@ func (qs *QuestionService) UpdateQuestion(ctx context.Context, req *schema.Quest
return questionInfo, tagerr
}
- // If it's not admin
- if !req.IsAdmin {
- //CheckChangeTag
-
- CheckTag, CheckTaglist := qs.CheckChangeReservedTag(ctx, oldTags, Tags)
- if !CheckTag {
+ // if user can not use reserved tag, old reserved tag can not be removed and new reserved tag can not be added.
+ if !req.CanUseReservedTag {
+ CheckOldTag, CheckNewTag, CheckOldTaglist, CheckNewTaglist := qs.CheckChangeReservedTag(ctx, oldTags, Tags)
+ if !CheckOldTag {
errMsg := fmt.Sprintf(`The reserved tag "%s" must be present.`,
- strings.Join(CheckTaglist, ","))
+ strings.Join(CheckOldTaglist, ","))
+ errorlist := make([]*validator.FormErrorField, 0)
+ errorlist = append(errorlist, &validator.FormErrorField{
+ ErrorField: "tags",
+ ErrorMsg: errMsg,
+ })
+ err = errors.BadRequest(reason.RequestFormatError).WithMsg(errMsg)
+ return errorlist, err
+ }
+ if !CheckNewTag {
+ errMsg := fmt.Sprintf(`"%s" can only be used by moderators.`,
+ strings.Join(CheckNewTaglist, ","))
errorlist := make([]*validator.FormErrorField, 0)
errorlist = append(errorlist, &validator.FormErrorField{
ErrorField: "tags",
@@ -373,7 +501,7 @@ func (qs *QuestionService) UpdateQuestion(ctx context.Context, req *schema.Quest
Log: req.EditSummary,
}
- if req.NoNeedReview || req.IsAdmin || dbinfo.UserID == req.UserID {
+ if req.NoNeedReview {
canUpdate = true
}
@@ -429,8 +557,15 @@ func (qs *QuestionService) GetQuestion(ctx context.Context, questionID, userID s
if err != nil {
return
}
+ if question.Status != entity.QuestionStatusClosed {
+ per.CanReopen = false
+ }
+ if question.Status == entity.QuestionStatusClosed {
+ per.CanClose = false
+ }
+ question.Description = htmltext.FetchExcerpt(question.HTML, "...", 240)
question.MemberActions = permission.GetQuestionPermission(ctx, userID, question.UserID,
- per.CanEdit, per.CanDelete, per.CanClose)
+ per.CanEdit, per.CanDelete, per.CanClose, per.CanReopen)
return question, nil
}
@@ -449,7 +584,7 @@ func (qs *QuestionService) ChangeTag(ctx context.Context, objectTagData *schema.
return qs.tagCommon.ObjectChangeTag(ctx, objectTagData)
}
-func (qs *QuestionService) CheckChangeReservedTag(ctx context.Context, oldobjectTagData, objectTagData []*entity.Tag) (bool, []string) {
+func (qs *QuestionService) CheckChangeReservedTag(ctx context.Context, oldobjectTagData, objectTagData []*entity.Tag) (bool, bool, []string, []string) {
return qs.tagCommon.CheckChangeReservedTag(ctx, oldobjectTagData, objectTagData)
}
@@ -872,3 +1007,54 @@ func (qs *QuestionService) changeQuestionToRevision(ctx context.Context, questio
}
return questionRevision, nil
}
+
+func (qs *QuestionService) SitemapCron(ctx context.Context) {
+ data := &schema.SiteMapList{}
+ questionNum, err := qs.questionRepo.GetQuestionCount(ctx)
+ if err != nil {
+ log.Error("GetQuestionCount error", err)
+ return
+ }
+ if questionNum <= schema.SitemapMaxSize {
+ questionIDList, err := qs.questionRepo.GetQuestionIDsPage(ctx, 0, int(questionNum))
+ if err != nil {
+ log.Error("GetQuestionIDsPage error", err)
+ return
+ }
+ data.QuestionIDs = questionIDList
+
+ } else {
+ nums := make([]int, 0)
+ totalpages := int(math.Ceil(float64(questionNum) / float64(schema.SitemapMaxSize)))
+ for i := 1; i <= totalpages; i++ {
+ siteMapPagedata := &schema.SiteMapPageList{}
+ nums = append(nums, i)
+ questionIDList, err := qs.questionRepo.GetQuestionIDsPage(ctx, i, int(schema.SitemapMaxSize))
+ if err != nil {
+ log.Error("GetQuestionIDsPage error", err)
+ return
+ }
+ siteMapPagedata.PageData = questionIDList
+ if setCacheErr := qs.SetCache(ctx, fmt.Sprintf(schema.SitemapPageCachekey, i), siteMapPagedata); setCacheErr != nil {
+ log.Errorf("set sitemap cron SetCache failed: %s", setCacheErr)
+ }
+ }
+ data.MaxPageNum = nums
+ }
+ if setCacheErr := qs.SetCache(ctx, schema.SitemapCachekey, data); setCacheErr != nil {
+ log.Errorf("set sitemap cron SetCache failed: %s", setCacheErr)
+ }
+}
+
+func (qs *QuestionService) SetCache(ctx context.Context, cachekey string, info interface{}) error {
+ infoStr, err := json.Marshal(info)
+ if err != nil {
+ return errors.InternalServer(reason.UnknownError).WithError(err).WithStack()
+ }
+
+ err = qs.data.Cache.SetString(ctx, cachekey, string(infoStr), schema.DashBoardCacheTime)
+ if err != nil {
+ return errors.InternalServer(reason.UnknownError).WithError(err).WithStack()
+ }
+ return nil
+}
diff --git a/internal/service/rank/rank_service.go b/internal/service/rank/rank_service.go
index 88db3347..111924e9 100644
--- a/internal/service/rank/rank_service.go
+++ b/internal/service/rank/rank_service.go
@@ -11,6 +11,8 @@ import (
"github.com/answerdev/answer/internal/service/activity_type"
"github.com/answerdev/answer/internal/service/config"
"github.com/answerdev/answer/internal/service/object_info"
+ "github.com/answerdev/answer/internal/service/permission"
+ "github.com/answerdev/answer/internal/service/role"
usercommon "github.com/answerdev/answer/internal/service/user_common"
"github.com/segmentfault/pacman/errors"
"github.com/segmentfault/pacman/log"
@@ -18,35 +20,7 @@ import (
)
const (
- QuestionAddRank = "rank.question.add"
- QuestionEditRank = "rank.question.edit"
- QuestionEditWithoutReviewRank = "rank.question.edit_without_review"
- QuestionDeleteRank = "rank.question.delete"
- QuestionVoteUpRank = "rank.question.vote_up"
- QuestionVoteDownRank = "rank.question.vote_down"
- AnswerAddRank = "rank.answer.add"
- AnswerEditRank = "rank.answer.edit"
- AnswerEditWithoutReviewRank = "rank.answer.edit_without_review"
- AnswerDeleteRank = "rank.answer.delete"
- AnswerAcceptRank = "rank.answer.accept"
- AnswerVoteUpRank = "rank.answer.vote_up"
- AnswerVoteDownRank = "rank.answer.vote_down"
- CommentAddRank = "rank.comment.add"
- CommentEditRank = "rank.comment.edit"
- CommentDeleteRank = "rank.comment.delete"
- CommentVoteUpRank = "rank.comment.vote_up"
- CommentVoteDownRank = "rank.comment.vote_down"
- ReportAddRank = "rank.report.add"
- TagAddRank = "rank.tag.add"
- TagEditRank = "rank.tag.edit"
- TagEditWithoutReviewRank = "rank.tag.edit_without_review"
- TagDeleteRank = "rank.tag.delete"
- TagSynonymRank = "rank.tag.synonym"
- LinkUrlLimitRank = "rank.link.url_limit"
- VoteDetailRank = "rank.vote.detail"
- AnswerAuditRank = "rank.answer.audit"
- QuestionAuditRank = "rank.question.audit"
- TagAuditRank = "rank.tag.audit"
+ PermissionPrefix = "rank."
)
type UserRankRepo interface {
@@ -60,6 +34,8 @@ type RankService struct {
configRepo config.ConfigRepo
userRankRepo UserRankRepo
objectInfoService *object_info.ObjService
+ roleService *role.UserRoleRelService
+ rolePowerService *role.RolePowerRelService
}
// NewRankService new rank service
@@ -67,12 +43,16 @@ func NewRankService(
userCommon *usercommon.UserCommon,
userRankRepo UserRankRepo,
objectInfoService *object_info.ObjService,
+ roleService *role.UserRoleRelService,
+ rolePowerService *role.RolePowerRelService,
configRepo config.ConfigRepo) *RankService {
return &RankService{
userCommon: userCommon,
configRepo: configRepo,
userRankRepo: userRankRepo,
objectInfoService: objectInfoService,
+ roleService: roleService,
+ rolePowerService: rolePowerService,
}
}
@@ -91,8 +71,8 @@ func (rs *RankService) CheckOperationPermission(ctx context.Context, userID stri
if !exist {
return false, nil
}
- // administrator have all permissions
- if userInfo.IsAdmin {
+ powerMapping := rs.getUserPowerMapping(ctx, userID)
+ if powerMapping[action] {
return true, nil
}
@@ -108,11 +88,12 @@ func (rs *RankService) CheckOperationPermission(ctx context.Context, userID stri
}
}
- return rs.checkUserRank(ctx, userInfo.ID, userInfo.Rank, action)
+ can = rs.checkUserRank(ctx, userInfo.ID, userInfo.Rank, PermissionPrefix+action)
+ return can, nil
}
// CheckOperationPermissions verify that the user has permission
-func (rs *RankService) CheckOperationPermissions(ctx context.Context, userID string, actions []string, objectID string) (
+func (rs *RankService) CheckOperationPermissions(ctx context.Context, userID string, actions []string) (
can []bool, err error) {
can = make([]bool, len(actions))
if len(userID) == 0 {
@@ -128,33 +109,33 @@ func (rs *RankService) CheckOperationPermissions(ctx context.Context, userID str
return can, nil
}
- objectOwner := false
- if len(objectID) > 0 {
- objectInfo, err := rs.objectInfoService.GetInfo(ctx, objectID)
- if err != nil {
- return can, err
- }
- // if the user is this object creator, the user can operate this object.
- if objectInfo != nil &&
- objectInfo.ObjectCreatorUserID == userID {
- objectOwner = true
- }
- }
-
+ powerMapping := rs.getUserPowerMapping(ctx, userID)
for idx, action := range actions {
- if userInfo.IsAdmin || objectOwner {
+ if powerMapping[action] {
can[idx] = true
continue
}
- meetRank, err := rs.checkUserRank(ctx, userInfo.ID, userInfo.Rank, action)
- if err != nil {
- log.Error(err)
- }
+ meetRank := rs.checkUserRank(ctx, userInfo.ID, userInfo.Rank, PermissionPrefix+action)
can[idx] = meetRank
}
return can, nil
}
+// CheckOperationObjectOwner check operation object owner
+func (rs *RankService) CheckOperationObjectOwner(ctx context.Context, userID, objectID string) bool {
+ objectInfo, err := rs.objectInfoService.GetInfo(ctx, objectID)
+ if err != nil {
+ log.Error(err)
+ return false
+ }
+ // if the user is this object creator, the user can operate this object.
+ if objectInfo != nil &&
+ objectInfo.ObjectCreatorUserID == userID {
+ return true
+ }
+ return false
+}
+
// CheckVotePermission verify that the user has vote permission
func (rs *RankService) CheckVotePermission(ctx context.Context, userID, objectID string, voteUp bool) (
can bool, err error) {
@@ -170,58 +151,75 @@ func (rs *RankService) CheckVotePermission(ctx context.Context, userID, objectID
if !exist {
return can, nil
}
- // administrator have all permissions
- if userInfo.IsAdmin {
- return true, nil
- }
-
objectInfo, err := rs.objectInfoService.GetInfo(ctx, objectID)
if err != nil {
return can, err
}
-
action := ""
switch objectInfo.ObjectType {
case constant.QuestionObjectType:
if voteUp {
- action = QuestionVoteUpRank
+ action = permission.QuestionVoteUp
} else {
- action = QuestionVoteDownRank
+ action = permission.QuestionVoteDown
}
case constant.AnswerObjectType:
if voteUp {
- action = AnswerVoteUpRank
+ action = permission.AnswerVoteUp
} else {
- action = AnswerVoteDownRank
+ action = permission.AnswerVoteDown
}
case constant.CommentObjectType:
if voteUp {
- action = CommentVoteUpRank
+ action = permission.CommentVoteUp
} else {
- action = CommentVoteDownRank
+ action = permission.CommentVoteDown
}
}
- meetRank, err := rs.checkUserRank(ctx, userInfo.ID, userInfo.Rank, action)
+ powerMapping := rs.getUserPowerMapping(ctx, userID)
+ if powerMapping[action] {
+ return true, nil
+ }
+
+ meetRank := rs.checkUserRank(ctx, userInfo.ID, userInfo.Rank, PermissionPrefix+action)
+ return meetRank, nil
+}
+
+// getUserPowerMapping get user power mapping
+func (rs *RankService) getUserPowerMapping(ctx context.Context, userID string) (powerMapping map[string]bool) {
+ powerMapping = make(map[string]bool, 0)
+ userRole, err := rs.roleService.GetUserRole(ctx, userID)
if err != nil {
log.Error(err)
+ return powerMapping
}
- return meetRank, nil
+ powers, err := rs.rolePowerService.GetRolePowerList(ctx, userRole)
+ if err != nil {
+ log.Error(err)
+ return powerMapping
+ }
+
+ for _, power := range powers {
+ powerMapping[power] = true
+ }
+ return powerMapping
}
// CheckRankPermission verify that the user meets the prestige criteria
func (rs *RankService) checkUserRank(ctx context.Context, userID string, userRank int, action string) (
- can bool, err error) {
+ can bool) {
// get the amount of rank required for the current operation
requireRank, err := rs.configRepo.GetInt(action)
if err != nil {
- return false, err
+ log.Error(err)
+ return false
}
if userRank < requireRank || requireRank < 0 {
log.Debugf("user %s want to do action %s, but rank %d < %d",
userID, action, userRank, requireRank)
- return false, nil
+ return false
}
- return true, nil
+ return true
}
// GetRankPersonalWithPage get personal comment list page
@@ -247,23 +245,24 @@ func (rs *RankService) GetRankPersonalWithPage(ctx context.Context, req *schema.
}
resp := make([]*schema.GetRankPersonalWithPageResp, 0)
for _, userRankInfo := range userRankPage {
+ if len(userRankInfo.ObjectID) == 0 || userRankInfo.ObjectID == "0" {
+ continue
+ }
commentResp := &schema.GetRankPersonalWithPageResp{
CreatedAt: userRankInfo.CreatedAt.Unix(),
ObjectID: userRankInfo.ObjectID,
Reputation: userRankInfo.Rank,
}
- if len(userRankInfo.ObjectID) > 0 {
- objInfo, err := rs.objectInfoService.GetInfo(ctx, userRankInfo.ObjectID)
- if err != nil {
- log.Error(err)
- } else {
- commentResp.RankType = activity_type.Format(userRankInfo.ActivityType)
- commentResp.ObjectType = objInfo.ObjectType
- commentResp.Title = objInfo.Title
- commentResp.Content = objInfo.Content
- commentResp.QuestionID = objInfo.QuestionID
- commentResp.AnswerID = objInfo.AnswerID
- }
+ objInfo, err := rs.objectInfoService.GetInfo(ctx, userRankInfo.ObjectID)
+ if err != nil {
+ log.Error(err)
+ } else {
+ commentResp.RankType = activity_type.Format(userRankInfo.ActivityType)
+ commentResp.ObjectType = objInfo.ObjectType
+ commentResp.Title = objInfo.Title
+ commentResp.Content = objInfo.Content
+ commentResp.QuestionID = objInfo.QuestionID
+ commentResp.AnswerID = objInfo.AnswerID
}
resp = append(resp, commentResp)
}
diff --git a/internal/service/report_backyard/report_backyard.go b/internal/service/report_backyard/report_backyard.go
index 69b87c85..94335d12 100644
--- a/internal/service/report_backyard/report_backyard.go
+++ b/internal/service/report_backyard/report_backyard.go
@@ -2,8 +2,10 @@ package report_backyard
import (
"context"
+
"github.com/answerdev/answer/internal/service/config"
"github.com/answerdev/answer/pkg/htmltext"
+ "github.com/segmentfault/pacman/log"
"github.com/answerdev/answer/internal/base/pager"
"github.com/answerdev/answer/internal/base/reason"
@@ -84,9 +86,15 @@ func (rs *ReportBackyardService) ListReportPage(ctx context.Context, dto schema.
// flagged users
flaggedUsers, err = rs.commonUser.BatchUserBasicInfoByID(ctx, flaggedUserIds)
+ if err != nil {
+ return nil, err
+ }
// flag users
users, err = rs.commonUser.BatchUserBasicInfoByID(ctx, userIds)
+ if err != nil {
+ return nil, err
+ }
for _, r := range resp {
r.ReportedUser = flaggedUsers[r.ReportedUserID]
r.ReportUser = users[r.UserID]
@@ -99,13 +107,13 @@ func (rs *ReportBackyardService) ListReportPage(ctx context.Context, dto schema.
// HandleReported handle the reported object
func (rs *ReportBackyardService) HandleReported(ctx context.Context, req schema.ReportHandleReq) (err error) {
var (
- reported = entity.Report{}
+ reported *entity.Report
handleData = entity.Report{
FlaggedContent: req.FlaggedContent,
FlaggedType: req.FlaggedType,
Status: entity.ReportStatusCompleted,
}
- exist = false
+ exist bool
)
reported, exist, err = rs.reportRepo.GetByID(ctx, req.ID)
@@ -152,6 +160,7 @@ func (rs *ReportBackyardService) parseObject(ctx context.Context, resp *[]*schem
objIds, err = rs.commonRepo.GetObjectIDMap(r.ObjectID)
if err != nil {
+ log.Error(err)
continue
}
@@ -168,11 +177,19 @@ func (rs *ReportBackyardService) parseObject(ctx context.Context, resp *[]*schem
answerId, ok = objIds["answer"]
if ok {
answer, _, err = rs.answerRepo.GetAnswer(ctx, answerId)
+ if err != nil {
+ log.Error(err)
+ continue
+ }
}
commentId, ok = objIds["comment"]
if ok {
cmt, _, err = rs.commentCommonRepo.GetComment(ctx, commentId)
+ if err != nil {
+ log.Error(err)
+ continue
+ }
}
switch r.OType {
@@ -201,15 +218,20 @@ func (rs *ReportBackyardService) parseObject(ctx context.Context, resp *[]*schem
ReasonType: r.ReportType,
}
err = rs.configRepo.GetJsonConfigByIDAndSetToObject(r.ReportType, r.Reason)
+ if err != nil {
+ log.Error(err)
+ }
}
if r.FlaggedType > 0 {
r.FlaggedReason = &schema.ReasonItem{
ReasonType: r.FlaggedType,
}
- _ = rs.configRepo.GetJsonConfigByIDAndSetToObject(r.FlaggedType, r.FlaggedReason)
+ err = rs.configRepo.GetJsonConfigByIDAndSetToObject(r.FlaggedType, r.FlaggedReason)
+ if err != nil {
+ log.Error(err)
+ }
}
res[i] = r
}
- resp = &res
}
diff --git a/internal/service/report_common/report_common.go b/internal/service/report_common/report_common.go
index 1b8c59cc..a467bda4 100644
--- a/internal/service/report_common/report_common.go
+++ b/internal/service/report_common/report_common.go
@@ -11,7 +11,7 @@ import (
type ReportRepo interface {
AddReport(ctx context.Context, report *entity.Report) (err error)
GetReportListPage(ctx context.Context, query schema.GetReportListPageDTO) (reports []entity.Report, total int64, err error)
- GetByID(ctx context.Context, id string) (report entity.Report, exist bool, err error)
+ GetByID(ctx context.Context, id string) (report *entity.Report, exist bool, err error)
UpdateByID(ctx context.Context, id string, handleData entity.Report) (err error)
GetReportCount(ctx context.Context) (count int64, err error)
}
diff --git a/internal/service/report_handle_backyard/report_handle.go b/internal/service/report_handle_backyard/report_handle.go
index 358c2a4c..cbeea25c 100644
--- a/internal/service/report_handle_backyard/report_handle.go
+++ b/internal/service/report_handle_backyard/report_handle.go
@@ -32,7 +32,7 @@ func NewReportHandle(
}
// HandleObject this handle object status
-func (rh *ReportHandle) HandleObject(ctx context.Context, reported entity.Report, req schema.ReportHandleReq) (err error) {
+func (rh *ReportHandle) HandleObject(ctx context.Context, reported *entity.Report, req schema.ReportHandleReq) (err error) {
var (
objectID = reported.ObjectID
reportedUserID = reported.ReportedUserID
diff --git a/internal/service/revision_service.go b/internal/service/revision_service.go
index 84624cea..bb65db32 100644
--- a/internal/service/revision_service.go
+++ b/internal/service/revision_service.go
@@ -305,7 +305,7 @@ func (rs *RevisionService) GetUnreviewedRevisionPage(ctx context.Context, req *s
}
if exists {
var uinfo schema.UserBasicInfo
- err = copier.Copy(&uinfo, userInfo)
+ _ = copier.Copy(&uinfo, userInfo)
item.UnreviewedInfo.UserInfo = uinfo
}
revisionResp = append(revisionResp, item)
diff --git a/internal/service/role/power_service.go b/internal/service/role/power_service.go
new file mode 100644
index 00000000..43f3b706
--- /dev/null
+++ b/internal/service/role/power_service.go
@@ -0,0 +1,12 @@
+package role
+
+import (
+ "context"
+
+ "github.com/answerdev/answer/internal/entity"
+)
+
+// PowerRepo power repository
+type PowerRepo interface {
+ GetPowerList(ctx context.Context, power *entity.Power) (powers []*entity.Power, err error)
+}
diff --git a/internal/service/role/role_power_rel_service.go b/internal/service/role/role_power_rel_service.go
new file mode 100644
index 00000000..f152c154
--- /dev/null
+++ b/internal/service/role/role_power_rel_service.go
@@ -0,0 +1,39 @@
+package role
+
+import (
+ "context"
+)
+
+// RolePowerRelRepo rolePowerRel repository
+type RolePowerRelRepo interface {
+ GetRolePowerTypeList(ctx context.Context, roleID int) (powers []string, err error)
+}
+
+// RolePowerRelService user service
+type RolePowerRelService struct {
+ rolePowerRelRepo RolePowerRelRepo
+ userRoleRelService *UserRoleRelService
+}
+
+// NewRolePowerRelService new role power rel service
+func NewRolePowerRelService(rolePowerRelRepo RolePowerRelRepo,
+ userRoleRelService *UserRoleRelService) *RolePowerRelService {
+ return &RolePowerRelService{
+ rolePowerRelRepo: rolePowerRelRepo,
+ userRoleRelService: userRoleRelService,
+ }
+}
+
+// GetRolePowerList get role power list
+func (rs *RolePowerRelService) GetRolePowerList(ctx context.Context, roleID int) (powers []string, err error) {
+ return rs.rolePowerRelRepo.GetRolePowerTypeList(ctx, roleID)
+}
+
+// GetUserPowerList get list all
+func (rs *RolePowerRelService) GetUserPowerList(ctx context.Context, userID string) (powers []string, err error) {
+ roleID, err := rs.userRoleRelService.GetUserRole(ctx, userID)
+ if err != nil {
+ return nil, err
+ }
+ return rs.rolePowerRelRepo.GetRolePowerTypeList(ctx, roleID)
+}
diff --git a/internal/service/role/role_service.go b/internal/service/role/role_service.go
new file mode 100644
index 00000000..ae18f762
--- /dev/null
+++ b/internal/service/role/role_service.go
@@ -0,0 +1,84 @@
+package role
+
+import (
+ "context"
+
+ "github.com/answerdev/answer/internal/base/handler"
+ "github.com/answerdev/answer/internal/base/translator"
+ "github.com/answerdev/answer/internal/entity"
+ "github.com/answerdev/answer/internal/schema"
+ "github.com/jinzhu/copier"
+)
+
+const (
+ // Since there is currently no need to edit roles to add roles and other operations,
+ // the current role information is translated directly.
+ // Later on, when the relevant ability is available, it can be adjusted by the user himself.
+
+ RoleUserID = 1
+ RoleAdminID = 2
+ RoleModeratorID = 3
+
+ roleUserName = "User"
+ roleAdminName = "Admin"
+ roleModeratorName = "Moderator"
+
+ trRoleNameUser = "role.name.user"
+ trRoleNameAdmin = "role.name.admin"
+ trRoleNameModerator = "role.name.moderator"
+
+ trRoleDescriptionUser = "role.description.user"
+ trRoleDescriptionAdmin = "role.description.admin"
+ trRoleDescriptionModerator = "role.description.moderator"
+)
+
+// RoleRepo role repository
+type RoleRepo interface {
+ GetRoleAllList(ctx context.Context) (roles []*entity.Role, err error)
+ GetRoleAllMapping(ctx context.Context) (roleMapping map[int]*entity.Role, err error)
+}
+
+// RoleService user service
+type RoleService struct {
+ roleRepo RoleRepo
+}
+
+func NewRoleService(roleRepo RoleRepo) *RoleService {
+ return &RoleService{
+ roleRepo: roleRepo,
+ }
+}
+
+// GetRoleList get role list all
+func (rs *RoleService) GetRoleList(ctx context.Context) (resp []*schema.GetRoleResp, err error) {
+ roles, err := rs.roleRepo.GetRoleAllList(ctx)
+ if err != nil {
+ return
+ }
+
+ for _, role := range roles {
+ rs.translateRole(ctx, role)
+ }
+
+ resp = []*schema.GetRoleResp{}
+ _ = copier.Copy(&resp, roles)
+ return
+}
+
+func (rs *RoleService) GetRoleMapping(ctx context.Context) (roleMapping map[int]*entity.Role, err error) {
+ return rs.roleRepo.GetRoleAllMapping(ctx)
+}
+
+func (rs *RoleService) translateRole(ctx context.Context, role *entity.Role) {
+ switch role.Name {
+ case roleUserName:
+ role.Name = translator.GlobalTrans.Tr(handler.GetLangByCtx(ctx), trRoleNameUser)
+ role.Description = translator.GlobalTrans.Tr(handler.GetLangByCtx(ctx), trRoleDescriptionUser)
+ case roleAdminName:
+ role.Name = translator.GlobalTrans.Tr(handler.GetLangByCtx(ctx), trRoleNameAdmin)
+ role.Description = translator.GlobalTrans.Tr(handler.GetLangByCtx(ctx), trRoleDescriptionAdmin)
+ case roleModeratorName:
+ role.Name = translator.GlobalTrans.Tr(handler.GetLangByCtx(ctx), trRoleNameModerator)
+ role.Description = translator.GlobalTrans.Tr(handler.GetLangByCtx(ctx), trRoleDescriptionModerator)
+ }
+}
diff --git a/internal/service/role/user_role_rel_service.go b/internal/service/role/user_role_rel_service.go
new file mode 100644
index 00000000..7055b645
--- /dev/null
+++ b/internal/service/role/user_role_rel_service.go
@@ -0,0 +1,106 @@
+package role
+
+import (
+ "context"
+
+ "github.com/answerdev/answer/internal/entity"
+)
+
+// UserRoleRelRepo userRoleRel repository
+type UserRoleRelRepo interface {
+ SaveUserRoleRel(ctx context.Context, userID string, roleID int) (err error)
+ GetUserRoleRelList(ctx context.Context, userIDs []string) (userRoleRelList []*entity.UserRoleRel, err error)
+ GetUserRoleRelListByRoleID(ctx context.Context, roleIDs []int) (
+ userRoleRelList []*entity.UserRoleRel, err error)
+ GetUserRoleRel(ctx context.Context, userID string) (rolePowerRel *entity.UserRoleRel, exist bool, err error)
+}
+
+// UserRoleRelService user service
+type UserRoleRelService struct {
+ userRoleRelRepo UserRoleRelRepo
+ roleService *RoleService
+}
+
+// NewUserRoleRelService new user role rel service
+func NewUserRoleRelService(userRoleRelRepo UserRoleRelRepo, roleService *RoleService) *UserRoleRelService {
+ return &UserRoleRelService{
+ userRoleRelRepo: userRoleRelRepo,
+ roleService: roleService,
+ }
+}
+
+// SaveUserRole save user role
+func (us *UserRoleRelService) SaveUserRole(ctx context.Context, userID string, roleID int) (err error) {
+ return us.userRoleRelRepo.SaveUserRoleRel(ctx, userID, roleID)
+}
+
+// GetUserRoleMapping get user role mapping
+func (us *UserRoleRelService) GetUserRoleMapping(ctx context.Context, userIDs []string) (
+ userRoleMapping map[string]*entity.Role, err error) {
+ userRoleMapping = make(map[string]*entity.Role, 0)
+ roleMapping, err := us.roleService.GetRoleMapping(ctx)
+ if err != nil {
+ return userRoleMapping, err
+ }
+ if len(roleMapping) == 0 {
+ return userRoleMapping, nil
+ }
+
+ relMapping, err := us.GetUserRoleRelMapping(ctx, userIDs)
+ if err != nil {
+ return userRoleMapping, err
+ }
+
+ // default role is user
+ defaultRole := roleMapping[1]
+ for _, userID := range userIDs {
+ roleID, ok := relMapping[userID]
+ if !ok {
+ userRoleMapping[userID] = defaultRole
+ continue
+ }
+ userRoleMapping[userID] = roleMapping[roleID]
+ if userRoleMapping[userID] == nil {
+ userRoleMapping[userID] = defaultRole
+ }
+ }
+ return userRoleMapping, nil
+}
+
+// GetUserRoleRelMapping get user role rel mapping
+func (us *UserRoleRelService) GetUserRoleRelMapping(ctx context.Context, userIDs []string) (
+ userRoleRelMapping map[string]int, err error) {
+ userRoleRelMapping = make(map[string]int, 0)
+
+ relList, err := us.userRoleRelRepo.GetUserRoleRelList(ctx, userIDs)
+ if err != nil {
+ return userRoleRelMapping, err
+ }
+
+ for _, rel := range relList {
+ userRoleRelMapping[rel.UserID] = rel.RoleID
+ }
+ return userRoleRelMapping, nil
+}
+
+// GetUserRole get user role
+func (us *UserRoleRelService) GetUserRole(ctx context.Context, userID string) (roleID int, err error) {
+ rolePowerRel, exist, err := us.userRoleRelRepo.GetUserRoleRel(ctx, userID)
+ if err != nil {
+ return 0, err
+ }
+ if !exist {
+ // set default role
+ return 1, nil
+ }
+ return rolePowerRel.RoleID, nil
+}
+
+// GetUserByRoleID get user by role id
+func (us *UserRoleRelService) GetUserByRoleID(ctx context.Context, roleIDs []int) (rel []*entity.UserRoleRel, err error) {
+ rolePowerRels, err := us.userRoleRelRepo.GetUserRoleRelListByRoleID(ctx, roleIDs)
+ if err != nil {
+ return nil, err
+ }
+ return rolePowerRels, nil
+}
diff --git a/internal/service/search_parser/search_parser.go b/internal/service/search_parser/search_parser.go
index 3fbbba56..e90ab1f8 100644
--- a/internal/service/search_parser/search_parser.go
+++ b/internal/service/search_parser/search_parser.go
@@ -49,7 +49,7 @@ func (sp *SearchParser) ParseStructure(dto *schema.SearchDTO) (
all = 0
q = 0
a = 0
- withWords = []string{}
+ withWords []string
limitWords = 5
)
@@ -189,16 +189,16 @@ func (sp *SearchParser) parseUserID(query *string, currentUserID string) (userID
re := regexp.MustCompile(exprUserID)
res := re.FindStringSubmatch(q)
- if len(res) == 2 {
+ if strings.Index(q, exprMe) != -1 {
+ userID = currentUserID
+ q = strings.ReplaceAll(q, exprMe, "")
+ } else if len(res) == 2 {
name := res[1]
- user, has, err := sp.userCommon.GetUserBasicInfoByUserName(nil, name)
+ user, has, err := sp.userCommon.GetUserBasicInfoByUserName(context.TODO(), name)
if err == nil && has {
userID = user.ID
q = re.ReplaceAllString(q, "")
}
- } else if strings.Index(q, exprMe) != -1 {
- userID = currentUserID
- q = strings.ReplaceAll(q, exprMe, "")
}
*query = strings.TrimSpace(q)
return
@@ -247,7 +247,7 @@ func (sp *SearchParser) parseNotAccepted(query *string) (notAccepted bool) {
expr = `hasaccepted:no`
)
- if strings.Index(q, expr) != -1 {
+ if strings.Contains(q, expr) {
q = strings.ReplaceAll(q, expr, "")
notAccepted = true
}
@@ -263,7 +263,7 @@ func (sp *SearchParser) parseIsQuestion(query *string) (isQuestion bool) {
expr = `is:question`
)
- if strings.Index(q, expr) == 0 {
+ if strings.Contains(q, expr) {
q = strings.ReplaceAll(q, expr, "")
isQuestion = true
}
@@ -316,9 +316,9 @@ func (sp *SearchParser) parseAccepted(query *string) (accepted bool) {
expr = `isaccepted:yes`
)
- if strings.Index(q, expr) != -1 {
+ if strings.Contains(q, expr) {
accepted = true
- strings.ReplaceAll(q, expr, "")
+ q = strings.ReplaceAll(q, expr, "")
}
*query = strings.TrimSpace(q)
@@ -350,7 +350,7 @@ func (sp *SearchParser) parseIsAnswer(query *string) (isAnswer bool) {
expr = `is:answer`
)
- if strings.Index(q, expr) != -1 {
+ if strings.Contains(q, expr) {
isAnswer = true
q = strings.ReplaceAll(q, expr, "")
}
diff --git a/internal/service/search_service.go b/internal/service/search_service.go
index 33d3f51e..61b622fb 100644
--- a/internal/service/search_service.go
+++ b/internal/service/search_service.go
@@ -31,19 +31,19 @@ func (ss *SearchService) Search(ctx context.Context, dto *schema.SearchDTO) (res
// search type
searchType,
- // search all
+ // search all
userID,
votes,
- // search questions
+ // search questions
notAccepted,
_,
views,
answers,
- // search answers
+ // search answers
accepted,
questionID,
_,
- // common fields
+ // common fields
tags,
words := ss.searchParser.ParseStructure(dto)
diff --git a/internal/service/siteinfo/siteinfo_service.go b/internal/service/siteinfo/siteinfo_service.go
index 256906fe..afbd7edd 100644
--- a/internal/service/siteinfo/siteinfo_service.go
+++ b/internal/service/siteinfo/siteinfo_service.go
@@ -18,65 +18,38 @@ import (
)
type SiteInfoService struct {
- siteInfoRepo siteinfo_common.SiteInfoRepo
- emailService *export.EmailService
- tagCommonService *tagcommon.TagCommonService
+ siteInfoRepo siteinfo_common.SiteInfoRepo
+ siteInfoCommonService *siteinfo_common.SiteInfoCommonService
+ emailService *export.EmailService
+ tagCommonService *tagcommon.TagCommonService
}
func NewSiteInfoService(
siteInfoRepo siteinfo_common.SiteInfoRepo,
+ siteInfoCommonService *siteinfo_common.SiteInfoCommonService,
emailService *export.EmailService,
tagCommonService *tagcommon.TagCommonService) *SiteInfoService {
return &SiteInfoService{
- siteInfoRepo: siteInfoRepo,
- emailService: emailService,
- tagCommonService: tagCommonService,
+ siteInfoRepo: siteInfoRepo,
+ siteInfoCommonService: siteInfoCommonService,
+ emailService: emailService,
+ tagCommonService: tagCommonService,
}
}
// GetSiteGeneral get site info general
func (s *SiteInfoService) GetSiteGeneral(ctx context.Context) (resp *schema.SiteGeneralResp, err error) {
- resp = &schema.SiteGeneralResp{}
- siteInfo, exist, err := s.siteInfoRepo.GetByType(ctx, constant.SiteTypeGeneral)
- if err != nil {
- log.Error(err)
- return resp, nil
- }
- if !exist {
- return resp, nil
- }
- _ = json.Unmarshal([]byte(siteInfo.Content), resp)
- return resp, nil
+ return s.siteInfoCommonService.GetSiteGeneral(ctx)
}
// GetSiteInterface get site info interface
func (s *SiteInfoService) GetSiteInterface(ctx context.Context) (resp *schema.SiteInterfaceResp, err error) {
- resp = &schema.SiteInterfaceResp{}
- siteInfo, exist, err := s.siteInfoRepo.GetByType(ctx, constant.SiteTypeInterface)
- if err != nil {
- log.Error(err)
- return resp, nil
- }
- if !exist {
- return resp, nil
- }
- _ = json.Unmarshal([]byte(siteInfo.Content), resp)
- return resp, nil
+ return s.siteInfoCommonService.GetSiteInterface(ctx)
}
// GetSiteBranding get site info branding
-func (s *SiteInfoService) GetSiteBranding(ctx context.Context) (resp *schema.SiteBrandingReq, err error) {
- resp = &schema.SiteBrandingReq{}
- siteInfo, exist, err := s.siteInfoRepo.GetByType(ctx, constant.SiteTypeBranding)
- if err != nil {
- log.Error(err)
- return resp, nil
- }
- if !exist {
- return resp, nil
- }
- _ = json.Unmarshal([]byte(siteInfo.Content), resp)
- return resp, nil
+func (s *SiteInfoService) GetSiteBranding(ctx context.Context) (resp *schema.SiteBrandingResp, err error) {
+ return s.siteInfoCommonService.GetSiteBranding(ctx)
}
// GetSiteWrite get site info write
@@ -104,16 +77,22 @@ func (s *SiteInfoService) GetSiteWrite(ctx context.Context) (resp *schema.SiteWr
// GetSiteLegal get site legal info
func (s *SiteInfoService) GetSiteLegal(ctx context.Context) (resp *schema.SiteLegalResp, err error) {
- resp = &schema.SiteLegalResp{}
- siteInfo, exist, err := s.siteInfoRepo.GetByType(ctx, constant.SiteTypeLegal)
- if err != nil {
- return nil, err
- }
- if !exist {
- return resp, nil
- }
- _ = json.Unmarshal([]byte(siteInfo.Content), resp)
- return resp, nil
+ return s.siteInfoCommonService.GetSiteLegal(ctx)
+}
+
+// GetSiteLogin get site login info
+func (s *SiteInfoService) GetSiteLogin(ctx context.Context) (resp *schema.SiteLoginResp, err error) {
+ return s.siteInfoCommonService.GetSiteLogin(ctx)
+}
+
+// GetSiteCustomCssHTML get site custom css html config
+func (s *SiteInfoService) GetSiteCustomCssHTML(ctx context.Context) (resp *schema.SiteCustomCssHTMLResp, err error) {
+ return s.siteInfoCommonService.GetSiteCustomCssHTML(ctx)
+}
+
+// GetSiteTheme get site theme config
+func (s *SiteInfoService) GetSiteTheme(ctx context.Context) (resp *schema.SiteThemeResp, err error) {
+ return s.siteInfoCommonService.GetSiteTheme(ctx)
}
func (s *SiteInfoService) SaveSiteGeneral(ctx context.Context, req schema.SiteGeneralReq) (err error) {
@@ -207,6 +186,39 @@ func (s *SiteInfoService) SaveSiteLegal(ctx context.Context, req *schema.SiteLeg
return s.siteInfoRepo.SaveByType(ctx, constant.SiteTypeLegal, data)
}
+// SaveSiteLogin save site legal configuration
+func (s *SiteInfoService) SaveSiteLogin(ctx context.Context, req *schema.SiteLoginReq) (err error) {
+ content, _ := json.Marshal(req)
+ data := &entity.SiteInfo{
+ Type: constant.SiteTypeLogin,
+ Content: string(content),
+ Status: 1,
+ }
+ return s.siteInfoRepo.SaveByType(ctx, constant.SiteTypeLogin, data)
+}
+
+// SaveSiteCustomCssHTML save site custom html configuration
+func (s *SiteInfoService) SaveSiteCustomCssHTML(ctx context.Context, req *schema.SiteCustomCssHTMLReq) (err error) {
+ content, _ := json.Marshal(req)
+ data := &entity.SiteInfo{
+ Type: constant.SiteTypeCustomCssHTML,
+ Content: string(content),
+ Status: 1,
+ }
+ return s.siteInfoRepo.SaveByType(ctx, constant.SiteTypeCustomCssHTML, data)
+}
+
+// SaveSiteTheme save site custom html configuration
+func (s *SiteInfoService) SaveSiteTheme(ctx context.Context, req *schema.SiteThemeReq) (err error) {
+ content, _ := json.Marshal(req)
+ data := &entity.SiteInfo{
+ Type: constant.SiteTypeTheme,
+ Content: string(content),
+ Status: 1,
+ }
+ return s.siteInfoRepo.SaveByType(ctx, constant.SiteTypeTheme, data)
+}
+
// GetSMTPConfig get smtp config
func (s *SiteInfoService) GetSMTPConfig(ctx context.Context) (
resp *schema.GetSMTPConfigResp, err error,
@@ -241,3 +253,45 @@ func (s *SiteInfoService) UpdateSMTPConfig(ctx context.Context, req *schema.Upda
}
return
}
+
+func (s *SiteInfoService) GetSeo(ctx context.Context) (resp *schema.SiteSeoResp, err error) {
+ resp = &schema.SiteSeoResp{}
+ loginConfig, err := s.GetSiteLogin(ctx)
+ if err != nil {
+ log.Error(err)
+ return resp, nil
+ }
+ // If the site is set to privacy mode, prohibit crawling any page.
+ if loginConfig.LoginRequired {
+ resp.Robots = "User-agent: *\nDisallow: /"
+ return resp, nil
+ }
+
+ resp = &schema.SiteSeoResp{}
+ siteInfo, exist, err := s.siteInfoRepo.GetByType(ctx, constant.SiteTypeSeo)
+ if err != nil {
+ log.Error(err)
+ return resp, nil
+ }
+ if !exist {
+ return resp, nil
+ }
+ _ = json.Unmarshal([]byte(siteInfo.Content), resp)
+ return resp, nil
+}
+
+func (s *SiteInfoService) SaveSeo(ctx context.Context, req schema.SiteSeoReq) (err error) {
+ var (
+ siteType = constant.SiteTypeSeo
+ content []byte
+ )
+ content, _ = json.Marshal(req)
+
+ data := entity.SiteInfo{
+ Type: siteType,
+ Content: string(content),
+ }
+
+ err = s.siteInfoRepo.SaveByType(ctx, siteType, &data)
+ return
+}
diff --git a/internal/service/siteinfo_common/siteinfo.go b/internal/service/siteinfo_common/siteinfo.go
deleted file mode 100644
index 1c501d2d..00000000
--- a/internal/service/siteinfo_common/siteinfo.go
+++ /dev/null
@@ -1,12 +0,0 @@
-package siteinfo_common
-
-import (
- "context"
-
- "github.com/answerdev/answer/internal/entity"
-)
-
-type SiteInfoRepo interface {
- SaveByType(ctx context.Context, siteType string, data *entity.SiteInfo) (err error)
- GetByType(ctx context.Context, siteType string) (siteInfo *entity.SiteInfo, exist bool, err error)
-}
diff --git a/internal/service/siteinfo_common/siteinfo_service.go b/internal/service/siteinfo_common/siteinfo_service.go
index 97d6006f..b61e59ff 100644
--- a/internal/service/siteinfo_common/siteinfo_service.go
+++ b/internal/service/siteinfo_common/siteinfo_service.go
@@ -5,13 +5,22 @@ import (
"encoding/json"
"github.com/answerdev/answer/internal/base/constant"
+ "github.com/answerdev/answer/internal/entity"
"github.com/answerdev/answer/internal/schema"
)
+//go:generate mockgen -source=./siteinfo_service.go -destination=../mock/siteinfo_repo_mock.go -package=mock
+type SiteInfoRepo interface {
+ SaveByType(ctx context.Context, siteType string, data *entity.SiteInfo) (err error)
+ GetByType(ctx context.Context, siteType string) (siteInfo *entity.SiteInfo, exist bool, err error)
+}
+
+// SiteInfoCommonService site info common service
type SiteInfoCommonService struct {
siteInfoRepo SiteInfoRepo
}
+// NewSiteInfoCommonService new site info common service
func NewSiteInfoCommonService(siteInfoRepo SiteInfoRepo) *SiteInfoCommonService {
return &SiteInfoCommonService{
siteInfoRepo: siteInfoRepo,
@@ -21,69 +30,95 @@ func NewSiteInfoCommonService(siteInfoRepo SiteInfoRepo) *SiteInfoCommonService
// GetSiteGeneral get site info general
func (s *SiteInfoCommonService) GetSiteGeneral(ctx context.Context) (resp *schema.SiteGeneralResp, err error) {
resp = &schema.SiteGeneralResp{}
- siteInfo, exist, err := s.siteInfoRepo.GetByType(ctx, constant.SiteTypeGeneral)
- if err != nil {
- return resp, err
+ if err = s.getSiteInfoByType(ctx, constant.SiteTypeGeneral, resp); err != nil {
+ return nil, err
}
- if !exist {
- return resp, nil
- }
- _ = json.Unmarshal([]byte(siteInfo.Content), resp)
return resp, nil
}
// GetSiteInterface get site info interface
func (s *SiteInfoCommonService) GetSiteInterface(ctx context.Context) (resp *schema.SiteInterfaceResp, err error) {
resp = &schema.SiteInterfaceResp{}
- siteInfo, exist, err := s.siteInfoRepo.GetByType(ctx, constant.SiteTypeInterface)
- if err != nil {
- return resp, err
+ if err = s.getSiteInfoByType(ctx, constant.SiteTypeInterface, resp); err != nil {
+ return nil, err
}
- if !exist {
- return resp, nil
- }
- _ = json.Unmarshal([]byte(siteInfo.Content), resp)
return resp, nil
}
// GetSiteBranding get site info branding
func (s *SiteInfoCommonService) GetSiteBranding(ctx context.Context) (resp *schema.SiteBrandingResp, err error) {
resp = &schema.SiteBrandingResp{}
- siteInfo, exist, err := s.siteInfoRepo.GetByType(ctx, constant.SiteTypeBranding)
- if err != nil {
- return resp, err
+ if err = s.getSiteInfoByType(ctx, constant.SiteTypeBranding, resp); err != nil {
+ return nil, err
}
- if !exist {
- return resp, nil
- }
- _ = json.Unmarshal([]byte(siteInfo.Content), resp)
return resp, nil
}
// GetSiteWrite get site info write
func (s *SiteInfoCommonService) GetSiteWrite(ctx context.Context) (resp *schema.SiteWriteResp, err error) {
resp = &schema.SiteWriteResp{}
- siteInfo, exist, err := s.siteInfoRepo.GetByType(ctx, constant.SiteTypeWrite)
- if err != nil {
- return resp, err
+ if err = s.getSiteInfoByType(ctx, constant.SiteTypeWrite, resp); err != nil {
+ return nil, err
}
- if !exist {
- return resp, nil
- }
- _ = json.Unmarshal([]byte(siteInfo.Content), resp)
return resp, nil
}
// GetSiteLegal get site info write
func (s *SiteInfoCommonService) GetSiteLegal(ctx context.Context) (resp *schema.SiteLegalResp, err error) {
resp = &schema.SiteLegalResp{}
- siteInfo, exist, err := s.siteInfoRepo.GetByType(ctx, constant.SiteTypeLegal)
- if err != nil {
+ if err = s.getSiteInfoByType(ctx, constant.SiteTypeLegal, resp); err != nil {
return nil, err
}
- if !exist {
- return resp, nil
- }
- _ = json.Unmarshal([]byte(siteInfo.Content), resp)
return resp, nil
}
+
+// GetSiteLogin get site login config
+func (s *SiteInfoCommonService) GetSiteLogin(ctx context.Context) (resp *schema.SiteLoginResp, err error) {
+ resp = &schema.SiteLoginResp{}
+ if err = s.getSiteInfoByType(ctx, constant.SiteTypeLogin, resp); err != nil {
+ return nil, err
+ }
+ return resp, nil
+}
+
+// GetSiteCustomCssHTML get site custom css html config
+func (s *SiteInfoCommonService) GetSiteCustomCssHTML(ctx context.Context) (resp *schema.SiteCustomCssHTMLResp, err error) {
+ resp = &schema.SiteCustomCssHTMLResp{}
+ if err = s.getSiteInfoByType(ctx, constant.SiteTypeCustomCssHTML, resp); err != nil {
+ return nil, err
+ }
+ return resp, nil
+}
+
+// GetSiteTheme get site theme
+func (s *SiteInfoCommonService) GetSiteTheme(ctx context.Context) (resp *schema.SiteThemeResp, err error) {
+ resp = &schema.SiteThemeResp{
+ ThemeOptions: schema.GetThemeOptions,
+ }
+ if err = s.getSiteInfoByType(ctx, constant.SiteTypeTheme, resp); err != nil {
+ return nil, err
+ }
+ resp.TrTheme(ctx)
+ return resp, nil
+}
+
+// GetSiteSeo get site seo
+func (s *SiteInfoCommonService) GetSiteSeo(ctx context.Context) (resp *schema.SiteSeoReq, err error) {
+ resp = &schema.SiteSeoReq{}
+ if err = s.getSiteInfoByType(ctx, constant.SiteTypeSeo, resp); err != nil {
+ return nil, err
+ }
+ return resp, nil
+}
+
+func (s *SiteInfoCommonService) getSiteInfoByType(ctx context.Context, siteType string, resp interface{}) (err error) {
+ siteInfo, exist, err := s.siteInfoRepo.GetByType(ctx, siteType)
+ if err != nil {
+ return err
+ }
+ if !exist {
+ return nil
+ }
+ _ = json.Unmarshal([]byte(siteInfo.Content), resp)
+ return nil
+}
diff --git a/internal/service/siteinfo_common/siteinfo_service_test.go b/internal/service/siteinfo_common/siteinfo_service_test.go
new file mode 100644
index 00000000..4d80c67a
--- /dev/null
+++ b/internal/service/siteinfo_common/siteinfo_service_test.go
@@ -0,0 +1,32 @@
+package siteinfo_common
+
+import (
+ "context"
+ "testing"
+
+ "github.com/answerdev/answer/internal/base/constant"
+ "github.com/answerdev/answer/internal/entity"
+ "github.com/answerdev/answer/internal/service/mock"
+ "github.com/golang/mock/gomock"
+ "github.com/stretchr/testify/assert"
+)
+
+var (
+ mockSiteInfoRepo *mock.MockSiteInfoRepo
+)
+
+func mockInit(ctl *gomock.Controller) {
+ mockSiteInfoRepo = mock.NewMockSiteInfoRepo(ctl)
+ mockSiteInfoRepo.EXPECT().GetByType(gomock.Any(), constant.SiteTypeGeneral).
+ Return(&entity.SiteInfo{Content: `{"name":"name"}`}, true, nil)
+}
+
+func TestSiteInfoCommonService_GetSiteGeneral(t *testing.T) {
+ ctl := gomock.NewController(t)
+ defer ctl.Finish()
+ mockInit(ctl)
+ siteInfoCommonService := NewSiteInfoCommonService(mockSiteInfoRepo)
+ resp, err := siteInfoCommonService.GetSiteGeneral(context.TODO())
+ assert.NoError(t, err)
+ assert.Equal(t, resp.Name, "name")
+}
diff --git a/internal/service/tag/tag_service.go b/internal/service/tag/tag_service.go
index 1504f4fe..c0bc6aca 100644
--- a/internal/service/tag/tag_service.go
+++ b/internal/service/tag/tag_service.go
@@ -105,6 +105,7 @@ func (ts *TagService) GetTagInfo(ctx context.Context, req *schema.GetTagInfoReq)
resp.DisplayName = tagInfo.DisplayName
resp.OriginalText = tagInfo.OriginalText
resp.ParsedText = tagInfo.ParsedText
+ resp.Description = htmltext.FetchExcerpt(tagInfo.ParsedText, "...", 240)
resp.FollowCount = tagInfo.FollowCount
resp.QuestionCount = tagInfo.QuestionCount
resp.Recommend = tagInfo.Recommend
@@ -215,6 +216,9 @@ func (ts *TagService) UpdateTagSynonym(ctx context.Context, req *schema.UpdateTa
// find all exist tag
for _, item := range req.SynonymTagList {
+ if item.SlugName == mainTagInfo.SlugName {
+ return errors.BadRequest(reason.TagCannotSetSynonymAsItself)
+ }
addSynonymTagList = append(addSynonymTagList, item.SlugName)
}
tagListInDB, err := ts.tagCommonService.GetTagListByNames(ctx, addSynonymTagList)
diff --git a/internal/service/tag_common/tag_common.go b/internal/service/tag_common/tag_common.go
index 028131f2..6af946bb 100644
--- a/internal/service/tag_common/tag_common.go
+++ b/internal/service/tag_common/tag_common.go
@@ -24,7 +24,7 @@ type TagCommonRepo interface {
AddTagList(ctx context.Context, tagList []*entity.Tag) (err error)
GetTagListByIDs(ctx context.Context, ids []string) (tagList []*entity.Tag, err error)
GetTagBySlugName(ctx context.Context, slugName string) (tagInfo *entity.Tag, exist bool, err error)
- GetTagListByName(ctx context.Context, name string, limit int, hasReserved bool) (tagList []*entity.Tag, err error)
+ GetTagListByName(ctx context.Context, name string, hasReserved bool) (tagList []*entity.Tag, err error)
GetTagListByNames(ctx context.Context, names []string) (tagList []*entity.Tag, err error)
GetTagByID(ctx context.Context, tagID string, includeDeleted bool) (tag *entity.Tag, exist bool, err error)
GetTagPage(ctx context.Context, page, pageSize int, tag *entity.Tag, queryCond string) (tagList []*entity.Tag, total int64, err error)
@@ -79,7 +79,7 @@ func NewTagCommonService(
// SearchTagLike get tag list all
func (ts *TagCommonService) SearchTagLike(ctx context.Context, req *schema.SearchTagLikeReq) (resp []schema.SearchTagLikeResp, err error) {
- tags, err := ts.tagCommonRepo.GetTagListByName(ctx, req.Tag, 5, req.IsAdmin)
+ tags, err := ts.tagCommonRepo.GetTagListByName(ctx, req.Tag, req.IsAdmin)
if err != nil {
return
}
@@ -162,6 +162,9 @@ func (ts *TagCommonService) SetTagsAttribute(ctx context.Context, tags []string,
default:
return
}
+ if err != nil {
+ return err
+ }
err = ts.tagCommonRepo.UpdateTagsAttribute(ctx, tagslist, attribute, false)
if err != nil {
return err
@@ -212,6 +215,9 @@ func (ts *TagCommonService) ExistRecommend(ctx context.Context, tags []*schema.T
// GetObjectTag get object tag
func (ts *TagCommonService) GetObjectTag(ctx context.Context, objectId string) (objTags []*schema.TagResp, err error) {
tagsInfoList, err := ts.GetObjectEntityTag(ctx, objectId)
+ if err != nil {
+ return nil, err
+ }
return ts.TagFormat(ctx, tagsInfoList)
}
@@ -436,16 +442,17 @@ func (ts *TagCommonService) CheckTagsIsChange(ctx context.Context, tagNameList,
check[item] = true
}
for _, value := range check {
- if value == false {
+ if !value {
return true
}
}
return false
}
-func (ts *TagCommonService) CheckChangeReservedTag(ctx context.Context, oldobjectTagData, objectTagData []*entity.Tag) (bool, []string) {
+func (ts *TagCommonService) CheckChangeReservedTag(ctx context.Context, oldobjectTagData, objectTagData []*entity.Tag) (bool, bool, []string, []string) {
reservedTagsMap := make(map[string]bool)
needTagsMap := make([]string, 0)
+ notNeedTagsMap := make([]string, 0)
for _, tag := range objectTagData {
if tag.Reserved {
reservedTagsMap[tag.SlugName] = true
@@ -456,14 +463,27 @@ func (ts *TagCommonService) CheckChangeReservedTag(ctx context.Context, oldobjec
_, ok := reservedTagsMap[tag.SlugName]
if !ok {
needTagsMap = append(needTagsMap, tag.SlugName)
+ } else {
+ reservedTagsMap[tag.SlugName] = false
}
}
}
- if len(needTagsMap) > 0 {
- return false, needTagsMap
+
+ for k, v := range reservedTagsMap {
+ if v {
+ notNeedTagsMap = append(notNeedTagsMap, k)
+ }
}
- return true, []string{}
+ if len(needTagsMap) > 0 {
+ return false, true, needTagsMap, []string{}
+ }
+
+ if len(notNeedTagsMap) > 0 {
+ return true, false, []string{}, notNeedTagsMap
+ }
+
+ return true, true, []string{}, []string{}
}
// ObjectChangeTag change object tag list
diff --git a/internal/service/uploader/upload.go b/internal/service/uploader/upload.go
index ffd3824e..3a3df6e8 100644
--- a/internal/service/uploader/upload.go
+++ b/internal/service/uploader/upload.go
@@ -119,12 +119,14 @@ func (us *UploaderService) AvatarThumbFile(ctx *gin.Context, uploadPath, fileNam
return avatarfile, fmt.Errorf("img extension not exist")
}
err = imaging.Encode(&buf, new_image, FormatExts[fileSuffix])
-
if err != nil {
return avatarfile, errors.InternalServer(reason.UnknownError).WithError(err).WithStack()
}
thumbReader := bytes.NewReader(buf.Bytes())
- dir.CreateDirIfNotExist(path.Join(us.serviceConfig.UploadPath, avatarThumbSubPath))
+ err = dir.CreateDirIfNotExist(path.Join(us.serviceConfig.UploadPath, avatarThumbSubPath))
+ if err != nil {
+ return nil, errors.InternalServer(reason.UnknownError).WithError(err).WithStack()
+ }
avatarFilePath := path.Join(avatarThumbSubPath, thumbFileName)
savefilePath := path.Join(us.serviceConfig.UploadPath, avatarFilePath)
out, err := os.Create(savefilePath)
diff --git a/internal/service/user_backyard/user_backyard.go b/internal/service/user_backyard/user_backyard.go
index b0aa8ad9..a00198b1 100644
--- a/internal/service/user_backyard/user_backyard.go
+++ b/internal/service/user_backyard/user_backyard.go
@@ -3,31 +3,55 @@ package user_backyard
import (
"context"
"fmt"
+ "net/mail"
+ "strings"
"time"
+ "unicode"
"github.com/answerdev/answer/internal/base/pager"
"github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/internal/entity"
"github.com/answerdev/answer/internal/schema"
+ "github.com/answerdev/answer/internal/service/auth"
+ "github.com/answerdev/answer/internal/service/role"
+ usercommon "github.com/answerdev/answer/internal/service/user_common"
"github.com/jinzhu/copier"
"github.com/segmentfault/pacman/errors"
+ "github.com/segmentfault/pacman/log"
+ "golang.org/x/crypto/bcrypt"
)
// UserBackyardRepo user repository
type UserBackyardRepo interface {
UpdateUserStatus(ctx context.Context, userID string, userStatus, mailStatus int, email string) (err error)
GetUserInfo(ctx context.Context, userID string) (user *entity.User, exist bool, err error)
- GetUserPage(ctx context.Context, page, pageSize int, user *entity.User, query string) (users []*entity.User, total int64, err error)
+ GetUserInfoByEmail(ctx context.Context, email string) (user *entity.User, exist bool, err error)
+ GetUserPage(ctx context.Context, page, pageSize int, user *entity.User,
+ usernameOrDisplayName string, isStaff bool) (users []*entity.User, total int64, err error)
+ AddUser(ctx context.Context, user *entity.User) (err error)
+ UpdateUserPassword(ctx context.Context, userID string, password string) (err error)
}
// UserBackyardService user service
type UserBackyardService struct {
- userRepo UserBackyardRepo
+ userRepo UserBackyardRepo
+ userRoleRelService *role.UserRoleRelService
+ authService *auth.AuthService
+ userCommonService *usercommon.UserCommon
}
-func NewUserBackyardService(userRepo UserBackyardRepo) *UserBackyardService {
+// NewUserBackyardService new user backyard service
+func NewUserBackyardService(
+ userRepo UserBackyardRepo,
+ userRoleRelService *role.UserRoleRelService,
+ authService *auth.AuthService,
+ userCommonService *usercommon.UserCommon,
+) *UserBackyardService {
return &UserBackyardService{
- userRepo: userRepo,
+ userRepo: userRepo,
+ userRoleRelService: userRoleRelService,
+ authService: authService,
+ userCommonService: userCommonService,
}
}
@@ -62,6 +86,81 @@ func (us *UserBackyardService) UpdateUserStatus(ctx context.Context, req *schema
return us.userRepo.UpdateUserStatus(ctx, userInfo.ID, userInfo.Status, userInfo.MailStatus, userInfo.EMail)
}
+// UpdateUserRole update user role
+func (us *UserBackyardService) UpdateUserRole(ctx context.Context, req *schema.UpdateUserRoleReq) (err error) {
+ // Users cannot modify their roles
+ if req.UserID == req.LoginUserID {
+ return errors.BadRequest(reason.UserCannotUpdateYourRole)
+ }
+
+ err = us.userRoleRelService.SaveUserRole(ctx, req.UserID, req.RoleID)
+ if err != nil {
+ return err
+ }
+
+ us.authService.RemoveAllUserTokens(ctx, req.UserID)
+ return
+}
+
+// AddUser add user
+func (us *UserBackyardService) AddUser(ctx context.Context, req *schema.AddUserReq) (err error) {
+ _, has, err := us.userRepo.GetUserInfoByEmail(ctx, req.Email)
+ if err != nil {
+ return err
+ }
+ if has {
+ return errors.BadRequest(reason.EmailDuplicate)
+ }
+
+ hashPwd, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
+ if err != nil {
+ return err
+ }
+
+ userInfo := &entity.User{}
+ userInfo.EMail = req.Email
+ userInfo.DisplayName = req.DisplayName
+ userInfo.Pass = string(hashPwd)
+
+ userInfo.Username, err = us.userCommonService.MakeUsername(ctx, userInfo.DisplayName)
+ if err != nil {
+ return err
+ }
+ userInfo.MailStatus = entity.EmailStatusAvailable
+ userInfo.Status = entity.UserStatusAvailable
+ userInfo.Rank = 1
+
+ err = us.userRepo.AddUser(ctx, userInfo)
+ if err != nil {
+ return err
+ }
+ return
+}
+
+// UpdateUserPassword update user password
+func (us *UserBackyardService) UpdateUserPassword(ctx context.Context, req *schema.UpdateUserPasswordReq) (err error) {
+ userInfo, exist, err := us.userRepo.GetUserInfo(ctx, req.UserID)
+ if err != nil {
+ return err
+ }
+ if !exist {
+ return errors.BadRequest(reason.UserNotFound)
+ }
+
+ hashPwd, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
+ if err != nil {
+ return err
+ }
+
+ err = us.userRepo.UpdateUserPassword(ctx, userInfo.ID, string(hashPwd))
+ if err != nil {
+ return err
+ }
+ // logout this user
+ us.authService.RemoveAllUserTokens(ctx, req.UserID)
+ return
+}
+
// GetUserInfo get user one
func (us *UserBackyardService) GetUserInfo(ctx context.Context, userID string) (resp *schema.GetUserInfoResp, err error) {
user, exist, err := us.userRepo.GetUserInfo(ctx, userID)
@@ -91,7 +190,29 @@ func (us *UserBackyardService) GetUserPage(ctx context.Context, req *schema.GetU
user.Status = entity.UserStatusDeleted
}
- users, total, err := us.userRepo.GetUserPage(ctx, req.Page, req.PageSize, user, req.Query)
+ if len(req.Query) > 0 {
+ if email, e := mail.ParseAddress(req.Query); e == nil {
+ user.EMail = email.Address
+ req.Query = ""
+ } else if strings.HasPrefix(req.Query, "user:") {
+ id := strings.TrimSpace(strings.TrimPrefix(req.Query, "user:"))
+ idSearch := true
+ for _, r := range id {
+ if !unicode.IsDigit(r) {
+ idSearch = false
+ break
+ }
+ }
+ if idSearch {
+ user.ID = id
+ req.Query = ""
+ } else {
+ req.Query = id
+ }
+ }
+ }
+
+ users, total, err := us.userRepo.GetUserPage(ctx, req.Page, req.PageSize, user, req.Query, req.Staff)
if err != nil {
return
}
@@ -121,5 +242,28 @@ func (us *UserBackyardService) GetUserPage(ctx context.Context, req *schema.GetU
}
resp = append(resp, t)
}
+ us.setUserRoleInfo(ctx, resp)
return pager.NewPageModel(total, resp), nil
}
+
+func (us *UserBackyardService) setUserRoleInfo(ctx context.Context, resp []*schema.GetUserPageResp) {
+ var userIDs []string
+ for _, u := range resp {
+ userIDs = append(userIDs, u.UserID)
+ }
+
+ userRoleMapping, err := us.userRoleRelService.GetUserRoleMapping(ctx, userIDs)
+ if err != nil {
+ log.Error(err)
+ return
+ }
+
+ for _, u := range resp {
+ r := userRoleMapping[u.UserID]
+ if r == nil {
+ continue
+ }
+ u.RoleID = r.ID
+ u.RoleName = r.Name
+ }
+}
diff --git a/internal/service/user_common/user.go b/internal/service/user_common/user.go
index 281a74a9..bc33a1ba 100644
--- a/internal/service/user_common/user.go
+++ b/internal/service/user_common/user.go
@@ -2,9 +2,17 @@ package usercommon
import (
"context"
+ "encoding/hex"
+ "math/rand"
+ "regexp"
+ "strings"
+ "github.com/Chain-Zhang/pinyin"
+ "github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/internal/entity"
"github.com/answerdev/answer/internal/schema"
+ "github.com/answerdev/answer/pkg/checker"
+ "github.com/segmentfault/pacman/errors"
)
type UserRepo interface {
@@ -80,7 +88,6 @@ func (us *UserCommon) BatchUserBasicInfoByID(ctx context.Context, IDs []string)
func (us *UserCommon) FormatUserBasicInfo(ctx context.Context, userInfo *entity.User) *schema.UserBasicInfo {
userBasicInfo := &schema.UserBasicInfo{}
userBasicInfo.ID = userInfo.ID
- userBasicInfo.IsAdmin = userInfo.IsAdmin
userBasicInfo.Username = userInfo.Username
userBasicInfo.Rank = userInfo.Rank
userBasicInfo.DisplayName = userInfo.DisplayName
@@ -95,3 +102,41 @@ func (us *UserCommon) FormatUserBasicInfo(ctx context.Context, userInfo *entity.
}
return userBasicInfo
}
+
+// MakeUsername
+// Generate a unique Username based on the displayName
+func (us *UserCommon) MakeUsername(ctx context.Context, displayName string) (username string, err error) {
+ // Chinese processing
+ if has := checker.IsChinese(displayName); has {
+ str, err := pinyin.New(displayName).Split("").Mode(pinyin.WithoutTone).Convert()
+ if err != nil {
+ return "", errors.BadRequest(reason.UsernameInvalid)
+ } else {
+ displayName = str
+ }
+ }
+
+ username = strings.ReplaceAll(displayName, " ", "_")
+ username = strings.ToLower(username)
+ suffix := ""
+
+ re := regexp.MustCompile(`^[a-z0-9._-]{4,30}$`)
+ match := re.MatchString(username)
+ if !match {
+ return "", errors.BadRequest(reason.UsernameInvalid)
+ }
+
+ for {
+ _, has, err := us.userRepo.GetByUsername(ctx, username+suffix)
+ if err != nil {
+ return "", err
+ }
+ if !has {
+ break
+ }
+ bytes := make([]byte, 2)
+ _, _ = rand.Read(bytes)
+ suffix = hex.EncodeToString(bytes)
+ }
+ return username + suffix, nil
+}
diff --git a/internal/service/user_service.go b/internal/service/user_service.go
index 3466170e..391294c0 100644
--- a/internal/service/user_service.go
+++ b/internal/service/user_service.go
@@ -2,14 +2,10 @@ package service
import (
"context"
- "encoding/hex"
"encoding/json"
"fmt"
- "math/rand"
- "regexp"
- "strings"
+ "time"
- "github.com/Chain-Zhang/pinyin"
"github.com/answerdev/answer/internal/base/handler"
"github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/internal/base/translator"
@@ -17,12 +13,13 @@ import (
"github.com/answerdev/answer/internal/entity"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/activity"
+ "github.com/answerdev/answer/internal/service/activity_common"
"github.com/answerdev/answer/internal/service/auth"
"github.com/answerdev/answer/internal/service/export"
+ "github.com/answerdev/answer/internal/service/role"
"github.com/answerdev/answer/internal/service/service_config"
"github.com/answerdev/answer/internal/service/siteinfo_common"
usercommon "github.com/answerdev/answer/internal/service/user_common"
- "github.com/answerdev/answer/pkg/checker"
"github.com/google/uuid"
"github.com/segmentfault/pacman/errors"
"github.com/segmentfault/pacman/log"
@@ -33,28 +30,37 @@ import (
// UserService user service
type UserService struct {
- userRepo usercommon.UserRepo
- userActivity activity.UserActiveActivityRepo
- serviceConfig *service_config.ServiceConfig
- emailService *export.EmailService
- authService *auth.AuthService
- siteInfoService *siteinfo_common.SiteInfoCommonService
+ userCommonService *usercommon.UserCommon
+ userRepo usercommon.UserRepo
+ userActivity activity.UserActiveActivityRepo
+ activityRepo activity_common.ActivityRepo
+ serviceConfig *service_config.ServiceConfig
+ emailService *export.EmailService
+ authService *auth.AuthService
+ siteInfoService *siteinfo_common.SiteInfoCommonService
+ userRoleService *role.UserRoleRelService
}
func NewUserService(userRepo usercommon.UserRepo,
userActivity activity.UserActiveActivityRepo,
+ activityRepo activity_common.ActivityRepo,
emailService *export.EmailService,
authService *auth.AuthService,
serviceConfig *service_config.ServiceConfig,
siteInfoService *siteinfo_common.SiteInfoCommonService,
+ userRoleService *role.UserRoleRelService,
+ userCommonService *usercommon.UserCommon,
) *UserService {
return &UserService{
- userRepo: userRepo,
- userActivity: userActivity,
- emailService: emailService,
- serviceConfig: serviceConfig,
- authService: authService,
- siteInfoService: siteInfoService,
+ userCommonService: userCommonService,
+ userRepo: userRepo,
+ userActivity: userActivity,
+ activityRepo: activityRepo,
+ emailService: emailService,
+ serviceConfig: serviceConfig,
+ authService: authService,
+ siteInfoService: siteInfoService,
+ userRoleService: userRoleService,
}
}
@@ -67,9 +73,14 @@ func (us *UserService) GetUserInfoByUserID(ctx context.Context, token, userID st
if !exist {
return nil, errors.BadRequest(reason.UserNotFound)
}
+ roleID, err := us.userRoleService.GetUserRole(ctx, userInfo.ID)
+ if err != nil {
+ log.Error(err)
+ }
resp = &schema.GetUserToSetShowResp{}
resp.GetFromUserEntity(userInfo)
resp.AccessToken = token
+ resp.IsAdmin = roleID == role.RoleAdminID
return resp, nil
}
@@ -108,19 +119,24 @@ func (us *UserService) EmailLogin(ctx context.Context, req *schema.UserEmailLogi
log.Error("UpdateLastLoginDate", err.Error())
}
+ roleID, err := us.userRoleService.GetUserRole(ctx, userInfo.ID)
+ if err != nil {
+ log.Error(err)
+ }
+
resp = &schema.GetUserResp{}
resp.GetFromUserEntity(userInfo)
userCacheInfo := &entity.UserCacheInfo{
UserID: userInfo.ID,
EmailStatus: userInfo.MailStatus,
UserStatus: userInfo.Status,
- IsAdmin: userInfo.IsAdmin,
+ IsAdmin: roleID == role.RoleAdminID,
}
resp.AccessToken, err = us.authService.SetUserCacheInfo(ctx, userCacheInfo)
if err != nil {
return nil, err
}
- resp.IsAdmin = userInfo.IsAdmin
+ resp.IsAdmin = userCacheInfo.IsAdmin
if resp.IsAdmin {
err = us.authService.SetCmsUserCacheInfo(ctx, resp.AccessToken, userCacheInfo)
if err != nil {
@@ -293,13 +309,14 @@ func (us *UserService) UserRegisterByEmail(ctx context.Context, registerUserInfo
if err != nil {
return nil, err
}
- userInfo.Username, err = us.makeUsername(ctx, registerUserInfo.Name)
+ userInfo.Username, err = us.userCommonService.MakeUsername(ctx, registerUserInfo.Name)
if err != nil {
return nil, err
}
userInfo.IPInfo = registerUserInfo.IP
userInfo.MailStatus = entity.EmailStatusToBeVerified
userInfo.Status = entity.UserStatusAvailable
+ userInfo.LastLoginDate = time.Now()
err = us.userRepo.AddUser(ctx, userInfo)
if err != nil {
return nil, err
@@ -318,6 +335,11 @@ func (us *UserService) UserRegisterByEmail(ctx context.Context, registerUserInfo
}
go us.emailService.Send(ctx, userInfo.EMail, title, body, code, data.ToJSONString())
+ roleID, err := us.userRoleService.GetUserRole(ctx, userInfo.ID)
+ if err != nil {
+ log.Error(err)
+ }
+
// return user info and token
resp = &schema.GetUserResp{}
resp.GetFromUserEntity(userInfo)
@@ -325,13 +347,13 @@ func (us *UserService) UserRegisterByEmail(ctx context.Context, registerUserInfo
UserID: userInfo.ID,
EmailStatus: userInfo.MailStatus,
UserStatus: userInfo.Status,
- IsAdmin: userInfo.IsAdmin,
+ IsAdmin: roleID == role.RoleAdminID,
}
resp.AccessToken, err = us.authService.SetUserCacheInfo(ctx, userCacheInfo)
if err != nil {
return nil, err
}
- resp.IsAdmin = userInfo.IsAdmin
+ resp.IsAdmin = userCacheInfo.IsAdmin
if resp.IsAdmin {
err = us.authService.SetCmsUserCacheInfo(ctx, resp.AccessToken, &entity.UserCacheInfo{UserID: userInfo.ID})
if err != nil {
@@ -406,13 +428,18 @@ func (us *UserService) UserVerifyEmail(ctx context.Context, req *schema.UserVeri
log.Error(err)
}
+ roleID, err := us.userRoleService.GetUserRole(ctx, userInfo.ID)
+ if err != nil {
+ log.Error(err)
+ }
+
resp = &schema.GetUserResp{}
resp.GetFromUserEntity(userInfo)
userCacheInfo := &entity.UserCacheInfo{
UserID: userInfo.ID,
EmailStatus: userInfo.MailStatus,
UserStatus: userInfo.Status,
- IsAdmin: userInfo.IsAdmin,
+ IsAdmin: roleID == role.RoleAdminID,
}
resp.AccessToken, err = us.authService.SetUserCacheInfo(ctx, userCacheInfo)
if err != nil {
@@ -422,7 +449,7 @@ func (us *UserService) UserVerifyEmail(ctx context.Context, req *schema.UserVeri
if err = us.authService.SetUserStatus(ctx, userCacheInfo); err != nil {
return nil, err
}
- resp.IsAdmin = userInfo.IsAdmin
+ resp.IsAdmin = userCacheInfo.IsAdmin
if resp.IsAdmin {
err = us.authService.SetCmsUserCacheInfo(ctx, resp.AccessToken, &entity.UserCacheInfo{UserID: userInfo.ID})
if err != nil {
@@ -432,44 +459,6 @@ func (us *UserService) UserVerifyEmail(ctx context.Context, req *schema.UserVeri
return resp, nil
}
-// makeUsername
-// Generate a unique Username based on the displayName
-func (us *UserService) makeUsername(ctx context.Context, displayName string) (username string, err error) {
- // Chinese processing
- if has := checker.IsChinese(displayName); has {
- str, err := pinyin.New(displayName).Split("").Mode(pinyin.WithoutTone).Convert()
- if err != nil {
- return "", err
- } else {
- displayName = str
- }
- }
-
- username = strings.ReplaceAll(displayName, " ", "_")
- username = strings.ToLower(username)
- suffix := ""
-
- re := regexp.MustCompile(`^[a-z0-9._-]{4,30}$`)
- match := re.MatchString(username)
- if !match {
- return "", errors.BadRequest(reason.UsernameInvalid)
- }
-
- for {
- _, has, err := us.userRepo.GetByUsername(ctx, username+suffix)
- if err != nil {
- return "", err
- }
- if !has {
- break
- }
- bytes := make([]byte, 2)
- _, _ = rand.Read(bytes)
- suffix = hex.EncodeToString(bytes)
- }
- return username + suffix, nil
-}
-
// verifyPassword
// Compare whether the password is correct
func (us *UserService) verifyPassword(ctx context.Context, LoginPass, UserPass string) bool {
@@ -572,3 +561,159 @@ func (us *UserService) getSiteUrl(ctx context.Context) string {
}
return siteGeneral.SiteUrl
}
+
+// UserRanking get user ranking
+func (us *UserService) UserRanking(ctx context.Context) (resp *schema.UserRankingResp, err error) {
+ limit := 20
+ endTime := time.Now()
+ startTime := endTime.AddDate(0, 0, -7)
+ userIDs, userIDExist := make([]string, 0), make(map[string]bool, 0)
+
+ // get most reputation users
+ rankStat, rankStatUserIDs, err := us.getActivityUserRankStat(ctx, startTime, endTime, limit, userIDExist)
+ if err != nil {
+ return nil, err
+ }
+ userIDs = append(userIDs, rankStatUserIDs...)
+
+ // get most vote users
+ voteStat, voteStatUserIDs, err := us.getActivityUserVoteStat(ctx, startTime, endTime, limit, userIDExist)
+ if err != nil {
+ return nil, err
+ }
+ userIDs = append(userIDs, voteStatUserIDs...)
+
+ // get all staff members
+ userRoleRels, staffUserIDs, err := us.getStaff(ctx, userIDExist)
+ if err != nil {
+ return nil, err
+ }
+ userIDs = append(userIDs, staffUserIDs...)
+
+ // get user information
+ userInfoMapping, err := us.getUserInfoMapping(ctx, userIDs)
+ if err != nil {
+ return nil, err
+ }
+ return us.warpStatRankingResp(userInfoMapping, rankStat, voteStat, userRoleRels), nil
+}
+
+func (us *UserService) getActivityUserRankStat(ctx context.Context, startTime, endTime time.Time, limit int,
+ userIDExist map[string]bool) (rankStat []*entity.ActivityUserRankStat, userIDs []string, err error) {
+ rankStat, err = us.activityRepo.GetUsersWhoHasGainedTheMostReputation(ctx, startTime, endTime, limit)
+ if err != nil {
+ return nil, nil, err
+ }
+ for _, stat := range rankStat {
+ if stat.Rank <= 0 {
+ continue
+ }
+ if userIDExist[stat.UserID] {
+ continue
+ }
+ userIDs = append(userIDs, stat.UserID)
+ userIDExist[stat.UserID] = true
+ }
+ return rankStat, userIDs, nil
+}
+
+func (us *UserService) getActivityUserVoteStat(ctx context.Context, startTime, endTime time.Time, limit int,
+ userIDExist map[string]bool) (voteStat []*entity.ActivityUserVoteStat, userIDs []string, err error) {
+ voteStat, err = us.activityRepo.GetUsersWhoHasVoteMost(ctx, startTime, endTime, limit)
+ if err != nil {
+ return nil, nil, err
+ }
+ for _, stat := range voteStat {
+ if stat.VoteCount <= 0 {
+ continue
+ }
+ if userIDExist[stat.UserID] {
+ continue
+ }
+ userIDs = append(userIDs, stat.UserID)
+ userIDExist[stat.UserID] = true
+ }
+ return voteStat, userIDs, nil
+}
+
+func (us *UserService) getStaff(ctx context.Context, userIDExist map[string]bool) (
+ userRoleRels []*entity.UserRoleRel, userIDs []string, err error) {
+ userRoleRels, err = us.userRoleService.GetUserByRoleID(ctx, []int{role.RoleAdminID, role.RoleModeratorID})
+ if err != nil {
+ return nil, nil, err
+ }
+ for _, rel := range userRoleRels {
+ if userIDExist[rel.UserID] {
+ continue
+ }
+ userIDs = append(userIDs, rel.UserID)
+ userIDExist[rel.UserID] = true
+ }
+ return userRoleRels, userIDs, nil
+}
+
+func (us *UserService) getUserInfoMapping(ctx context.Context, userIDs []string) (
+ userInfoMapping map[string]*entity.User, err error) {
+ userInfoMapping = make(map[string]*entity.User, 0)
+ if len(userIDs) == 0 {
+ return userInfoMapping, nil
+ }
+ userInfoList, err := us.userRepo.BatchGetByID(ctx, userIDs)
+ if err != nil {
+ return nil, err
+ }
+ for _, user := range userInfoList {
+ user.Avatar = schema.FormatAvatarInfo(user.Avatar)
+ userInfoMapping[user.ID] = user
+ }
+ return userInfoMapping, nil
+}
+
+func (us *UserService) warpStatRankingResp(
+ userInfoMapping map[string]*entity.User,
+ rankStat []*entity.ActivityUserRankStat,
+ voteStat []*entity.ActivityUserVoteStat,
+ userRoleRels []*entity.UserRoleRel) (resp *schema.UserRankingResp) {
+ resp = &schema.UserRankingResp{
+ UsersWithTheMostReputation: make([]*schema.UserRankingSimpleInfo, 0),
+ UsersWithTheMostVote: make([]*schema.UserRankingSimpleInfo, 0),
+ Staffs: make([]*schema.UserRankingSimpleInfo, 0),
+ }
+ for _, stat := range rankStat {
+ if stat.Rank <= 0 {
+ continue
+ }
+ if userInfo := userInfoMapping[stat.UserID]; userInfo != nil {
+ resp.UsersWithTheMostReputation = append(resp.UsersWithTheMostReputation, &schema.UserRankingSimpleInfo{
+ Username: userInfo.Username,
+ Rank: stat.Rank,
+ DisplayName: userInfo.DisplayName,
+ Avatar: userInfo.Avatar,
+ })
+ }
+ }
+ for _, stat := range voteStat {
+ if stat.VoteCount <= 0 {
+ continue
+ }
+ if userInfo := userInfoMapping[stat.UserID]; userInfo != nil {
+ resp.UsersWithTheMostVote = append(resp.UsersWithTheMostVote, &schema.UserRankingSimpleInfo{
+ Username: userInfo.Username,
+ VoteCount: stat.VoteCount,
+ DisplayName: userInfo.DisplayName,
+ Avatar: userInfo.Avatar,
+ })
+ }
+ }
+ for _, rel := range userRoleRels {
+ if userInfo := userInfoMapping[rel.UserID]; userInfo != nil {
+ resp.Staffs = append(resp.Staffs, &schema.UserRankingSimpleInfo{
+ Username: userInfo.Username,
+ Rank: userInfo.Rank,
+ DisplayName: userInfo.DisplayName,
+ Avatar: userInfo.Avatar,
+ })
+ }
+ }
+ return resp
+}
diff --git a/internal/service/vote_service.go b/internal/service/vote_service.go
index b77fe402..ae555bf0 100644
--- a/internal/service/vote_service.go
+++ b/internal/service/vote_service.go
@@ -180,6 +180,7 @@ func (vs *VoteService) ListUserVotes(ctx context.Context, req schema.GetVoteWith
objInfo, err = vs.objectService.GetInfo(ctx, voteInfo.ObjectID)
if err != nil {
log.Error(err)
+ continue
}
item := schema.GetVoteWithPageResp{
diff --git a/pkg/converter/markdown.go b/pkg/converter/markdown.go
new file mode 100644
index 00000000..79ebead6
--- /dev/null
+++ b/pkg/converter/markdown.go
@@ -0,0 +1,11 @@
+package converter
+
+import (
+ "github.com/gomarkdown/markdown"
+)
+
+// Markdown2HTML convert markdown to html
+func Markdown2HTML(md string) string {
+ html := markdown.ToHTML([]byte(md), nil, nil)
+ return string(html)
+}
diff --git a/pkg/converter/str.go b/pkg/converter/str.go
index 74d9b2ec..04415fe4 100644
--- a/pkg/converter/str.go
+++ b/pkg/converter/str.go
@@ -2,6 +2,7 @@ package converter
import (
"fmt"
+ "github.com/segmentfault/pacman/log"
"strconv"
)
@@ -24,3 +25,30 @@ func StringToInt(str string) int {
func IntToString(data int64) string {
return fmt.Sprintf("%d", data)
}
+
+// InterfaceToString converts data to string
+// It will be used in template render
+func InterfaceToString(data interface{}) string {
+ switch t := data.(type) {
+ case int:
+ i := data.(int)
+ return strconv.Itoa(i)
+ case int8:
+ i := data.(int8)
+ return strconv.Itoa(int(i))
+ case int16:
+ i := data.(int16)
+ return strconv.Itoa(int(i))
+ case int32:
+ i := data.(int32)
+ return string(i)
+ case int64:
+ i := data.(int64)
+ return strconv.FormatInt(i, 10)
+ case string:
+ return data.(string)
+ default:
+ log.Warn("can't convert type:", t)
+ }
+ return ""
+}
diff --git a/pkg/day/day.go b/pkg/day/day.go
new file mode 100644
index 00000000..c698e789
--- /dev/null
+++ b/pkg/day/day.go
@@ -0,0 +1,173 @@
+package day
+
+import (
+ "time"
+)
+
+var placeholder = map[string]string{
+ "YY": "06", // 06 year
+ "YYYY": "2006", // 2006 full year
+ "M": "1", // 1-12 month
+ "MM": "01", // 01-12 month
+ "MMM": "Jan", // Jan-Dec month
+ "MMMM": "January", // January-December month
+ "D": "2", // 1-31 date
+ "DD": "02", // 01-31 date preset 0
+ "H": "15", // 00-23 hour 24
+ "HH": "15", // 00-23 hour 24
+ "h": "3", // 1-12 hour 12
+ "hh": "03", // 01-12 hour 12
+ "m": "4", // 0-59 minute
+ "mm": "04", // 00-59 minute
+ "s": "5", // 0-59 second
+ "ss": "05", // 00-59 second
+ "A": "PM", // AM / PM
+ "a": "pm", // am / pm
+ "[at]": "at", // at string
+}
+
+func Format(unix int64, format, tz string) (formatted string) {
+ /*l := len(placeholders) - 1
+ for i := l; i >= 0; i-- {
+ format = strings.ReplaceAll(format, placeholders[i].old, placeholders[i].new)
+ }*/
+ toFormat := ""
+ from := []rune(format)
+ for len(from) > 0 {
+ to, suffix := nextStdChunk(from)
+ toFormat += string(to)
+ from = suffix
+ }
+
+ _, _ = time.LoadLocation(tz)
+ formatted = time.Unix(unix, 0).Format(toFormat)
+ return
+}
+
+func nextStdChunk(from []rune) (to, suffix []rune) {
+ if len(from) == 0 {
+ to = []rune{}
+ suffix = []rune{}
+ return
+ }
+
+ s := string(from[0])
+ old := ""
+
+ switch s {
+ case "Y":
+ if len(from) >= 4 && string(from[:4]) == "YYYY" {
+ old = "YYYY"
+ } else if len(from) >= 2 && string(from[:2]) == "YY" {
+ old = "YY"
+ }
+ case "M":
+ for i := 4; i > 0; i-- {
+ if len(from) >= i {
+ switch string(from[:i]) {
+ case "MMMM":
+ old = "MMMM"
+ case "MMM":
+ old = "MMM"
+ case "MM":
+ old = "MM"
+ case "M":
+ old = "M"
+ }
+ }
+ if old != "" {
+ break
+ }
+ }
+ case "D":
+ for i := 2; i >= 0; i-- {
+ if len(from) >= i {
+ switch string(from[:i]) {
+ case "DD":
+ old = "DD"
+ case "D":
+ old = "D"
+ }
+ }
+ if old != "" {
+ break
+ }
+ }
+ case "H":
+ for i := 2; i >= 0; i-- {
+ if len(from) >= i {
+ switch string(from[:i]) {
+ case "HH":
+ old = "HH"
+ case "H":
+ old = "H"
+ }
+ }
+ if old != "" {
+ break
+ }
+ }
+ case "h":
+ for i := 2; i >= 0; i-- {
+ if len(from) >= i {
+ switch string(from[:i]) {
+ case "hh":
+ old = "hh"
+ case "h":
+ old = "h"
+ }
+ }
+ if old != "" {
+ break
+ }
+ }
+ case "m":
+ for i := 2; i >= 0; i-- {
+ if len(from) >= i {
+ switch string(from[:i]) {
+ case "mm":
+ old = "mm"
+ case "m":
+ old = "m"
+ }
+ }
+ if old != "" {
+ break
+ }
+ }
+ case "s":
+ for i := 2; i >= 0; i-- {
+ if len(from) >= i {
+ switch string(from[:i]) {
+ case "ss":
+ old = "ss"
+ case "s":
+ old = "s"
+ }
+ }
+ if old != "" {
+ break
+ }
+ }
+ case "A":
+ old = "A"
+ case "a":
+ old = "a"
+ case "[":
+ if len(from) >= 4 && string(from[:4]) == "[at]" {
+ old = "[at]"
+ }
+ default:
+ old = s
+ }
+
+ tos, ok := placeholder[old]
+ if !ok {
+ to = []rune(old)
+ } else {
+ to = []rune(tos)
+ }
+
+ suffix = from[len([]rune(old)):]
+ return
+}
diff --git a/pkg/day/day_test.go b/pkg/day/day_test.go
new file mode 100644
index 00000000..91cfc815
--- /dev/null
+++ b/pkg/day/day_test.go
@@ -0,0 +1,16 @@
+package day
+
+import (
+ "github.com/stretchr/testify/assert"
+ "testing"
+ "time"
+)
+
+func TestFormat(t *testing.T) {
+ sec := time.Now().Unix()
+ tz := "Asia/Shanghai"
+ actual := Format(sec, "YYYY-MM-DD HH:mm:ss", tz)
+ _, _ = time.LoadLocation(tz)
+ expected := time.Unix(sec, 0).Format("2006-01-02 15:04:05")
+ assert.Equal(t, expected, actual)
+}
diff --git a/pkg/htmltext/htmltext.go b/pkg/htmltext/htmltext.go
index e0463e10..7f8e6e07 100644
--- a/pkg/htmltext/htmltext.go
+++ b/pkg/htmltext/htmltext.go
@@ -1,9 +1,14 @@
package htmltext
import (
- "github.com/grokify/html-strip-tags-go"
+ "io/ioutil"
+ "net/http"
+ "net/url"
"regexp"
"strings"
+
+ "github.com/gosimple/slug"
+ strip "github.com/grokify/html-strip-tags-go"
)
// ClearText clear HTML, get the clear text
@@ -40,6 +45,25 @@ func ClearText(html string) (text string) {
return
}
+func UrlTitle(title string) (text string) {
+ title = ClearEmoji(title)
+ title = slug.Make(title)
+ // title = strings.ReplaceAll(title, " ", "-")
+ title = url.QueryEscape(title)
+ return title
+}
+
+func ClearEmoji(s string) string {
+ ret := ""
+ rs := []rune(s)
+ for i := 0; i < len(rs); i++ {
+ if len(string(rs[i])) != 4 {
+ ret += string(rs[i])
+ }
+ }
+ return ret
+}
+
// FetchExcerpt return the excerpt from the HTML string
func FetchExcerpt(html, trimMarker string, limit int) (text string) {
if len(html) == 0 {
@@ -58,3 +82,16 @@ func FetchExcerpt(html, trimMarker string, limit int) (text string) {
text += trimMarker
return
}
+
+func GetPicByUrl(Url string) string {
+ res, err := http.Get(Url)
+ if err != nil {
+ return ""
+ }
+ defer res.Body.Close()
+ pix, err := ioutil.ReadAll(res.Body)
+ if err != nil {
+ return ""
+ }
+ return string(pix)
+}
diff --git a/pkg/htmltext/htmltext_test.go b/pkg/htmltext/htmltext_test.go
index a320ef12..9e405541 100644
--- a/pkg/htmltext/htmltext_test.go
+++ b/pkg/htmltext/htmltext_test.go
@@ -1,8 +1,10 @@
package htmltext
import (
- "github.com/stretchr/testify/assert"
"testing"
+
+ "github.com/davecgh/go-spew/spew"
+ "github.com/stretchr/testify/assert"
)
func TestClearText(t *testing.T) {
@@ -49,3 +51,15 @@ func TestFetchExcerpt(t *testing.T) {
text = FetchExcerpt("
hello你好😂world
", "...", 8) assert.Equal(t, expected, text) } + +func TestUrlTitle(t *testing.T) { + list := []string{ + "hello你好😂...", + "这是一个,标题,title", + } + for _, title := range list { + formatTitle := UrlTitle(title) + spew.Dump(formatTitle) + + } +} diff --git a/script/build_binary.sh b/script/build_binary.sh new file mode 100644 index 00000000..88a549a2 --- /dev/null +++ b/script/build_binary.sh @@ -0,0 +1,9 @@ +docker run \ + --rm \ + -e CGO_ENABLED=1 \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -v `pwd`:/go/src/github.com/answerdev/answer \ + -v `pwd`/sysroot:/sysroot \ + -w /go/src/github.com/answerdev/answer \ + goreleaser/goreleaser-cross \ + --rm-dist --skip-validate --skip-publish \ No newline at end of file diff --git a/ui/README.md b/ui/README.md index 42639ec5..f7fbb8aa 100644 --- a/ui/README.md +++ b/ui/README.md @@ -4,7 +4,7 @@ To learn more about the philosophy and goals of the project, visit [Answer](https://answer.dev). -### 📦 Prerequisites +## ⚙️ Prerequisites - [Node.js](https://nodejs.org/) `>=16.17` - [pnpm](https://pnpm.io/) `>=7` @@ -40,22 +40,78 @@ you can also manually visit it. when cloning repo, and run `pnpm install` to init dependencies. you can use project commands below: -- `pnpm run start` run Answer web locally. -- `pnpm run build` build Answer for production -- `pnpm run lint` lint and fix the code style +- `pnpm start` run Answer web locally. +- `pnpm build` build Answer for production +- `pnpm lint` lint and fix the code style +## 🌍 I18n(Multi-language) +If you need to add or edit a language entry, just go to the `/i18n/en_US.yaml` file, +all front-end language entries are placed under the `ui` field. -## 🖥 Environment Support +If you would like to help us with the i18n translation, please visit [Answer@Crowdin](https://crowdin.com/translate/answer) + +## 💡 Project instructions + +``` +. +├── cmd +├── configs +├── docs +├── i18n + ├── en_US.yaml (basic language file) + ├── i18n.yaml (language list) +├── internal +├── ... +└── ui (front-end project starts here) + ├── build (built results directory, usually without concern) + ├── public (html template for public) + ├── scripts (some scripting tools on front-end project) + ├── src (almost all front-end resources are here) + ├── assets (static resources) + ├── common (project information/data defined here) + ├── components (all components of the project) + ├── hooks (all hooks of the project) + ├── i18n (Initialize the front-end i18n) + ├── pages (all pages of the project) + ├── router (Project routing definition) + ├── services (all data api of the project) + ├── stores (all data stores of the project) + ├── utils (all utils of the project) +``` + +## 🤝 Contributing + +#### Fix Bug +If you find a bug, please don't hesitate to [submit an issue](https://github.com/answerdev/answer/issues) to us. +If you can fix it, please include a note with your issue submission. +If it is a bug definitely, you can submit your PR after we confirm it, which will ensure you don't do anything useless. + +#### Code Review & Comment +In our development, some codes are not logical we know. If you find it, please don't hesitate to submit PR to us. +In the same way, some function has no comment. We would appreciate it if you could help us supplement it. + +#### Translation +All our translations are placed in the i18n directory. + +1. If you find that the corresponding key in the language you are using does not have a translation, you can submit your translation. +2. If you want to submit a new language translation, please add your language to the `i18n.yaml` file. + +#### Features or Plugin +1. We developed the features for the plan based on the [roadmap](https://github.com/orgs/answerdev/projects/1). If you are suggestions for new functions, please confirm whether they have been planned. +2. Plugins will be available in the future, so stay tuned. + +## 📱Environment Support | [
](http://godban.github.io/browsers-support-badges/)
](http://godban.github.io/browsers-support-badges/)
](http://godban.github.io/browsers-support-badges/)
](http://godban.github.io/browsers-support-badges/)