diff --git a/.vscode/settings.json b/.vscode/settings.json index 93106f18..db563c27 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,6 @@ { "eslint.workingDirectories": [ "ui" - ] + ], + "explorer.autoReveal": "focusNoScroll" } diff --git a/Dockerfile b/Dockerfile index 66e0326a..ef700dee 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,7 +3,7 @@ LABEL maintainer="aichy@sf.com" ARG GOPROXY # ENV GOPROXY ${GOPROXY:-direct} -ENV GOPROXY=https://goproxy.io,direct +ENV GOPROXY=https://proxy.golang.com.cn,direct ENV GOPATH /go ENV GOROOT /usr/local/go diff --git a/Makefile b/Makefile index ce3d42c8..68e921b2 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ .PHONY: build clean ui -VERSION=1.1.0 +VERSION=1.1.1 BIN=answer DIR_SRC=./cmd/answer DOCKER_CMD=docker diff --git a/cmd/command.go b/cmd/command.go index e74cd636..3404ddfc 100644 --- a/cmd/command.go +++ b/cmd/command.go @@ -114,7 +114,7 @@ To run answer, use: fmt.Println("read config failed: ", err.Error()) return } - if err = migrations.Migrate(c.Data.Database, c.Data.Cache, upgradeVersion); err != nil { + if err = migrations.Migrate(c.Debug, c.Data.Database, c.Data.Cache, upgradeVersion); err != nil { fmt.Println("migrate failed: ", err.Error()) return } diff --git a/cmd/wire_gen.go b/cmd/wire_gen.go index 1c237435..63364974 100644 --- a/cmd/wire_gen.go +++ b/cmd/wire_gen.go @@ -46,6 +46,7 @@ import ( "github.com/answerdev/answer/internal/service/action" activity2 "github.com/answerdev/answer/internal/service/activity" activity_common2 "github.com/answerdev/answer/internal/service/activity_common" + "github.com/answerdev/answer/internal/service/activity_queue" "github.com/answerdev/answer/internal/service/answer_common" auth2 "github.com/answerdev/answer/internal/service/auth" "github.com/answerdev/answer/internal/service/collection_common" @@ -56,6 +57,7 @@ import ( export2 "github.com/answerdev/answer/internal/service/export" "github.com/answerdev/answer/internal/service/follow" meta2 "github.com/answerdev/answer/internal/service/meta" + "github.com/answerdev/answer/internal/service/notice_queue" notification2 "github.com/answerdev/answer/internal/service/notification" "github.com/answerdev/answer/internal/service/notification_common" "github.com/answerdev/answer/internal/service/object_info" @@ -128,8 +130,7 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database, userService := service.NewUserService(userRepo, userActiveActivityRepo, activityRepo, emailService, authService, siteInfoCommonService, userRoleRelService, userCommon, userExternalLoginService) captchaRepo := captcha.NewCaptchaRepo(dataData) captchaService := action.NewCaptchaService(captchaRepo) - uploaderService := uploader.NewUploaderService(serviceConf, siteInfoCommonService) - userController := controller.NewUserController(authService, userService, captchaService, emailService, uploaderService, siteInfoCommonService) + userController := controller.NewUserController(authService, userService, captchaService, emailService, siteInfoCommonService) commentRepo := comment.NewCommentRepo(dataData, uniqueIDRepo) commentCommonRepo := comment.NewCommentCommonRepo(dataData, uniqueIDRepo) answerRepo := answer.NewAnswerRepo(dataData, uniqueIDRepo, userRankRepo, activityRepo) @@ -139,22 +140,24 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database, tagRepo := tag.NewTagRepo(dataData, uniqueIDRepo) revisionRepo := revision.NewRevisionRepo(dataData, uniqueIDRepo) revisionService := revision_common.NewRevisionService(revisionRepo, userRepo) - tagCommonService := tag_common2.NewTagCommonService(tagCommonRepo, tagRelRepo, tagRepo, revisionService, siteInfoCommonService) + activityQueueService := activity_queue.NewActivityQueueService() + tagCommonService := tag_common2.NewTagCommonService(tagCommonRepo, tagRelRepo, tagRepo, revisionService, siteInfoCommonService, activityQueueService) objService := object_info.NewObjService(answerRepo, questionRepo, commentCommonRepo, tagCommonRepo, tagCommonService) voteRepo := activity_common.NewVoteRepo(dataData, activityRepo) - commentService := comment2.NewCommentService(commentRepo, commentCommonRepo, userCommon, objService, voteRepo, emailService, userRepo) + notificationQueueService := notice_queue.NewNotificationQueueService() + commentService := comment2.NewCommentService(commentRepo, commentCommonRepo, userCommon, objService, voteRepo, emailService, userRepo, notificationQueueService, activityQueueService) rolePowerRelRepo := role.NewRolePowerRelRepo(dataData) rolePowerRelService := role2.NewRolePowerRelService(rolePowerRelRepo, userRoleRelService) rankService := rank2.NewRankService(userCommon, userRankRepo, objService, userRoleRelService, rolePowerRelService, configService) - commentController := controller.NewCommentController(commentService, rankService) + commentController := controller.NewCommentController(commentService, rankService, captchaService) reportRepo := report.NewReportRepo(dataData, uniqueIDRepo) reportService := report2.NewReportService(reportRepo, objService) - reportController := controller.NewReportController(reportService, rankService) - serviceVoteRepo := activity.NewVoteRepo(dataData, uniqueIDRepo, configService, activityRepo, userRankRepo, voteRepo) - voteService := service.NewVoteService(serviceVoteRepo, uniqueIDRepo, configService, questionRepo, answerRepo, commentCommonRepo, objService) - voteController := controller.NewVoteController(voteService, rankService) + reportController := controller.NewReportController(reportService, rankService, captchaService) + serviceVoteRepo := activity.NewVoteRepo(dataData, activityRepo, userRankRepo, notificationQueueService) + voteService := service.NewVoteService(serviceVoteRepo, configService, questionRepo, answerRepo, commentCommonRepo, objService) + voteController := controller.NewVoteController(voteService, rankService, captchaService) followRepo := activity_common.NewFollowRepo(dataData, uniqueIDRepo, activityRepo) - tagService := tag2.NewTagService(tagRepo, tagCommonService, revisionService, followRepo, siteInfoCommonService) + tagService := tag2.NewTagService(tagRepo, tagCommonService, revisionService, followRepo, siteInfoCommonService, activityQueueService) tagController := controller.NewTagController(tagService, tagCommonService, rankService) followFollowRepo := activity.NewFollowRepo(dataData, uniqueIDRepo, activityRepo) followService := follow.NewFollowService(followFollowRepo, followRepo, tagCommonRepo) @@ -165,29 +168,27 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database, answerCommon := answercommon.NewAnswerCommon(answerRepo) metaRepo := meta.NewMetaRepo(dataData) metaService := meta2.NewMetaService(metaRepo) - questionCommon := questioncommon.NewQuestionCommon(questionRepo, answerRepo, voteRepo, followRepo, tagCommonService, userCommon, collectionCommon, answerCommon, metaService, configService, dataData) + questionCommon := questioncommon.NewQuestionCommon(questionRepo, answerRepo, voteRepo, followRepo, tagCommonService, userCommon, collectionCommon, answerCommon, metaService, configService, activityQueueService, dataData) collectionService := service.NewCollectionService(collectionRepo, collectionGroupRepo, questionCommon) collectionController := controller.NewCollectionController(collectionService) - answerActivityRepo := activity.NewAnswerActivityRepo(dataData, activityRepo, userRankRepo) - questionActivityRepo := activity.NewQuestionActivityRepo(dataData, activityRepo, userRankRepo) - answerActivityService := activity2.NewAnswerActivityService(answerActivityRepo, questionActivityRepo) - questionService := service.NewQuestionService(questionRepo, tagCommonService, questionCommon, userCommon, userRepo, revisionService, metaService, collectionCommon, answerActivityService, dataData, emailService) - answerService := service.NewAnswerService(answerRepo, questionRepo, questionCommon, userCommon, collectionCommon, userRepo, revisionService, answerActivityService, answerCommon, voteRepo, emailService, userRoleRelService) - questionController := controller.NewQuestionController(questionService, answerService, rankService) - dashboardService := dashboard.NewDashboardService(questionRepo, answerRepo, commentCommonRepo, voteRepo, userRepo, reportRepo, configService, siteInfoCommonService, serviceConf, dataData) - answerController := controller.NewAnswerController(answerService, rankService, dashboardService) + answerActivityRepo := activity.NewAnswerActivityRepo(dataData, activityRepo, userRankRepo, notificationQueueService) + answerActivityService := activity2.NewAnswerActivityService(answerActivityRepo, configService) + questionService := service.NewQuestionService(questionRepo, tagCommonService, questionCommon, userCommon, userRepo, revisionService, metaService, collectionCommon, answerActivityService, emailService, notificationQueueService, activityQueueService, siteInfoCommonService) + answerService := service.NewAnswerService(answerRepo, questionRepo, questionCommon, userCommon, collectionCommon, userRepo, revisionService, answerActivityService, answerCommon, voteRepo, emailService, userRoleRelService, notificationQueueService, activityQueueService) + questionController := controller.NewQuestionController(questionService, answerService, rankService, siteInfoCommonService, captchaService) + answerController := controller.NewAnswerController(answerService, rankService, captchaService) searchParser := search_parser.NewSearchParser(tagCommonService, userCommon) searchRepo := search_common.NewSearchRepo(dataData, uniqueIDRepo, userCommon) searchService := service.NewSearchService(searchParser, searchRepo) - searchController := controller.NewSearchController(searchService) - serviceRevisionService := service.NewRevisionService(revisionRepo, userCommon, questionCommon, answerService, objService, questionRepo, answerRepo, tagRepo, tagCommonService) + searchController := controller.NewSearchController(searchService, captchaService) + serviceRevisionService := service.NewRevisionService(revisionRepo, userCommon, questionCommon, answerService, objService, questionRepo, answerRepo, tagRepo, tagCommonService, notificationQueueService, activityQueueService) revisionController := controller.NewRevisionController(serviceRevisionService, rankService) rankController := controller.NewRankController(rankService) - reportHandle := report_handle_admin.NewReportHandle(questionCommon, commentRepo, configService) + reportHandle := report_handle_admin.NewReportHandle(questionCommon, commentRepo, configService, notificationQueueService) reportAdminService := report_admin.NewReportAdminService(reportRepo, userCommon, answerRepo, questionRepo, commentCommonRepo, reportHandle, configService, objService) controller_adminReportController := controller_admin.NewReportController(reportAdminService) userAdminRepo := user.NewUserAdminRepo(dataData, authRepo) - userAdminService := user_admin.NewUserAdminService(userAdminRepo, userRoleRelService, authService, userCommon, userActiveActivityRepo, siteInfoCommonService) + userAdminService := user_admin.NewUserAdminService(userAdminRepo, userRoleRelService, authService, userCommon, userActiveActivityRepo, siteInfoCommonService, emailService) userAdminController := controller_admin.NewUserAdminController(userAdminService) reasonRepo := reason.NewReasonRepo(configService) reasonService := reason2.NewReasonService(reasonRepo) @@ -195,36 +196,39 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database, themeController := controller_admin.NewThemeController() siteInfoService := siteinfo.NewSiteInfoService(siteInfoRepo, siteInfoCommonService, emailService, tagCommonService, configService, questionCommon) siteInfoController := controller_admin.NewSiteInfoController(siteInfoService) - siteinfoController := controller.NewSiteinfoController(siteInfoCommonService) + controllerSiteInfoController := controller.NewSiteInfoController(siteInfoCommonService) notificationRepo := notification.NewNotificationRepo(dataData) - notificationCommon := notificationcommon.NewNotificationCommon(dataData, notificationRepo, userCommon, activityRepo, followRepo, objService) + notificationCommon := notificationcommon.NewNotificationCommon(dataData, notificationRepo, userCommon, activityRepo, followRepo, objService, notificationQueueService) notificationService := notification2.NewNotificationService(dataData, notificationRepo, notificationCommon, revisionService) notificationController := controller.NewNotificationController(notificationService, rankService) + dashboardService := dashboard.NewDashboardService(questionRepo, answerRepo, commentCommonRepo, voteRepo, userRepo, reportRepo, configService, siteInfoCommonService, serviceConf, dataData) dashboardController := controller.NewDashboardController(dashboardService) + uploaderService := uploader.NewUploaderService(serviceConf, siteInfoCommonService) uploadController := controller.NewUploadController(uploaderService) - activityCommon := activity_common2.NewActivityCommon(activityRepo) activityActivityRepo := activity.NewActivityRepo(dataData, configService) + activityCommon := activity_common2.NewActivityCommon(activityRepo, activityQueueService) commentCommonService := comment_common.NewCommentCommonService(commentCommonRepo) activityService := activity2.NewActivityService(activityActivityRepo, userCommon, activityCommon, tagCommonService, objService, commentCommonService, revisionService, metaService, configService) - activityController := controller.NewActivityController(activityCommon, activityService) + activityController := controller.NewActivityController(activityService) roleController := controller_admin.NewRoleController(roleService) pluginConfigRepo := plugin_config.NewPluginConfigRepo(dataData) pluginCommonService := plugin_common.NewPluginCommonService(pluginConfigRepo, configService) pluginController := controller_admin.NewPluginController(pluginCommonService) permissionController := controller.NewPermissionController(rankService) - answerAPIRouter := router.NewAnswerAPIRouter(langController, userController, commentController, reportController, voteController, tagController, followController, collectionController, questionController, answerController, searchController, revisionController, rankController, controller_adminReportController, userAdminController, reasonController, themeController, siteInfoController, siteinfoController, notificationController, dashboardController, uploadController, activityController, roleController, pluginController, permissionController) + answerAPIRouter := router.NewAnswerAPIRouter(langController, userController, commentController, reportController, voteController, tagController, followController, collectionController, questionController, answerController, searchController, revisionController, rankController, controller_adminReportController, userAdminController, reasonController, themeController, siteInfoController, controllerSiteInfoController, notificationController, dashboardController, uploadController, activityController, roleController, pluginController, permissionController) swaggerRouter := router.NewSwaggerRouter(swaggerConf) - uiRouter := router.NewUIRouter(siteinfoController, siteInfoCommonService) + uiRouter := router.NewUIRouter(controllerSiteInfoController, siteInfoCommonService) authUserMiddleware := middleware.NewAuthUserMiddleware(authService, siteInfoCommonService) avatarMiddleware := middleware.NewAvatarMiddleware(serviceConf, uploaderService) - templateRenderController := templaterender.NewTemplateRenderController(questionService, userService, tagService, answerService, commentService, dataData, siteInfoCommonService) + shortIDMiddleware := middleware.NewShortIDMiddleware(siteInfoCommonService) + templateRenderController := templaterender.NewTemplateRenderController(questionService, userService, tagService, answerService, commentService, siteInfoCommonService, questionRepo) templateController := controller.NewTemplateController(templateRenderController, siteInfoCommonService) - templateRouter := router.NewTemplateRouter(templateController, templateRenderController, siteInfoController) + templateRouter := router.NewTemplateRouter(templateController, templateRenderController, siteInfoController, authUserMiddleware) connectorController := controller.NewConnectorController(siteInfoCommonService, emailService, userExternalLoginService) userCenterLoginService := user_external_login2.NewUserCenterLoginService(userRepo, userCommon, userExternalLoginRepo, userActiveActivityRepo, siteInfoCommonService) userCenterController := controller.NewUserCenterController(userCenterLoginService, siteInfoCommonService) pluginAPIRouter := router.NewPluginAPIRouter(connectorController, userCenterController) - ginEngine := server.NewHTTPServer(debug, staticRouter, answerAPIRouter, swaggerRouter, uiRouter, authUserMiddleware, avatarMiddleware, templateRouter, pluginAPIRouter) + ginEngine := server.NewHTTPServer(debug, staticRouter, answerAPIRouter, swaggerRouter, uiRouter, authUserMiddleware, avatarMiddleware, shortIDMiddleware, templateRouter, pluginAPIRouter) scheduledTaskManager := cron.NewScheduledTaskManager(siteInfoCommonService, questionService) application := newApplication(serverConf, ginEngine, scheduledTaskManager) return application, func() { diff --git a/docs/docs.go b/docs/docs.go index 96f9cf1a..a087c402 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -1,5 +1,5 @@ -// Package docs GENERATED BY SWAG; DO NOT EDIT -// This file was generated by swaggo/swag +// Code generated by swaggo/swag. DO NOT EDIT. + package docs import "github.com/swaggo/swag" @@ -49,7 +49,7 @@ const docTemplate = `{ "tags": [ "admin" ], - "summary": "AdminSearchAnswerList", + "summary": "AdminAnswerPage admin answer page", "parameters": [ { "type": "integer", @@ -379,7 +379,7 @@ const docTemplate = `{ "tags": [ "admin" ], - "summary": "AdminSearchList", + "summary": "AdminQuestionPage admin question page", "parameters": [ { "type": "integer", @@ -1578,6 +1578,52 @@ const docTemplate = `{ } } }, + "/answer/admin/api/user/activation": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "get user activation", + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "get user activation", + "parameters": [ + { + "type": "string", + "description": "user id", + "name": "user_id", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/schema.GetUserActivationResp" + } + } + } + ] + } + } + } + } + }, "/answer/admin/api/user/password": { "put": { "security": [ @@ -1695,6 +1741,42 @@ const docTemplate = `{ } } }, + "/answer/admin/api/users/activation": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "send user activation", + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "send user activation", + "parameters": [ + { + "description": "SendUserActivationReq", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.SendUserActivationReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, "/answer/admin/api/users/page": { "get": { "security": [ @@ -3444,7 +3526,7 @@ const docTemplate = `{ "ApiKeyAuth": [] } ], - "description": "user's vote", + "description": "get user personal votes", "consumes": [ "application/json" ], @@ -3454,7 +3536,7 @@ const docTemplate = `{ "tags": [ "Activity" ], - "summary": "user's votes", + "summary": "get user personal votes", "parameters": [ { "type": "integer", @@ -6345,7 +6427,8 @@ const docTemplate = `{ "properties": { "display_name": { "type": "string", - "maxLength": 30 + "maxLength": 30, + "minLength": 4 }, "email": { "type": "string", @@ -7082,7 +7165,7 @@ const docTemplate = `{ } }, "selected_level": { - "type": "integer" + "$ref": "#/definitions/schema.PrivilegeLevel" } } }, @@ -7397,6 +7480,14 @@ const docTemplate = `{ } } }, + "schema.GetUserActivationResp": { + "type": "object", + "properties": { + "activation_url": { + "type": "string" + } + } + }, "schema.GetUserPageResp": { "type": "object", "properties": { @@ -7573,11 +7664,24 @@ const docTemplate = `{ } } }, + "schema.PrivilegeLevel": { + "type": "integer", + "enum": [ + 1, + 2, + 3 + ], + "x-enum-varnames": [ + "PrivilegeLevel1", + "PrivilegeLevel2", + "PrivilegeLevel3" + ] + }, "schema.PrivilegeOption": { "type": "object", "properties": { "level": { - "type": "integer" + "$ref": "#/definitions/schema.PrivilegeLevel" }, "level_desc": { "type": "string" @@ -7663,11 +7767,11 @@ const docTemplate = `{ "schema.QuestionPageReq": { "type": "object", "properties": { - "inDays": { + "in_days": { "type": "integer", "minimum": 1 }, - "orderCond": { + "order": { "type": "string", "enum": [ "newest", @@ -7681,7 +7785,7 @@ const docTemplate = `{ "type": "integer", "minimum": 1 }, - "pageSize": { + "page_size": { "type": "integer", "minimum": 1 }, @@ -7992,7 +8096,11 @@ const docTemplate = `{ }, "user_info": { "description": "user info", - "$ref": "#/definitions/schema.UserBasicInfo" + "allOf": [ + { + "$ref": "#/definitions/schema.UserBasicInfo" + } + ] }, "vote_count": { "type": "integer" @@ -8004,7 +8112,11 @@ const docTemplate = `{ "properties": { "object": { "description": "this object", - "$ref": "#/definitions/schema.SearchObject" + "allOf": [ + { + "$ref": "#/definitions/schema.SearchObject" + } + ] }, "object_type": { "description": "object_type", @@ -8012,6 +8124,17 @@ const docTemplate = `{ } } }, + "schema.SendUserActivationReq": { + "type": "object", + "required": [ + "user_id" + ], + "properties": { + "user_id": { + "type": "string" + } + } + }, "schema.SiteBrandingReq": { "type": "object", "properties": { @@ -8186,7 +8309,7 @@ const docTemplate = `{ "type": "string" }, "site_seo": { - "$ref": "#/definitions/schema.SiteSeoReq" + "$ref": "#/definitions/schema.SiteSeoResp" }, "site_users": { "$ref": "#/definitions/schema.SiteUsersResp" @@ -8635,7 +8758,11 @@ const docTemplate = `{ "properties": { "avatar": { "description": "avatar", - "$ref": "#/definitions/schema.AvatarInfo" + "allOf": [ + { + "$ref": "#/definitions/schema.AvatarInfo" + } + ] }, "bio": { "description": "bio", @@ -8702,9 +8829,13 @@ const docTemplate = `{ ], "properties": { "level": { - "type": "integer", "maximum": 3, - "minimum": 1 + "minimum": 1, + "allOf": [ + { + "$ref": "#/definitions/schema.PrivilegeLevel" + } + ] } } }, @@ -9312,6 +9443,8 @@ var SwaggerInfo = &swag.Spec{ Description: "", InfoInstanceName: "swagger", SwaggerTemplate: docTemplate, + LeftDelim: "{{", + RightDelim: "}}", } func init() { diff --git a/docs/swagger.json b/docs/swagger.json index 8a0db0e1..dc60d471 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -37,7 +37,7 @@ "tags": [ "admin" ], - "summary": "AdminSearchAnswerList", + "summary": "AdminAnswerPage admin answer page", "parameters": [ { "type": "integer", @@ -367,7 +367,7 @@ "tags": [ "admin" ], - "summary": "AdminSearchList", + "summary": "AdminQuestionPage admin question page", "parameters": [ { "type": "integer", @@ -1566,6 +1566,52 @@ } } }, + "/answer/admin/api/user/activation": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "get user activation", + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "get user activation", + "parameters": [ + { + "type": "string", + "description": "user id", + "name": "user_id", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/schema.GetUserActivationResp" + } + } + } + ] + } + } + } + } + }, "/answer/admin/api/user/password": { "put": { "security": [ @@ -1683,6 +1729,42 @@ } } }, + "/answer/admin/api/users/activation": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "send user activation", + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "send user activation", + "parameters": [ + { + "description": "SendUserActivationReq", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.SendUserActivationReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, "/answer/admin/api/users/page": { "get": { "security": [ @@ -3432,7 +3514,7 @@ "ApiKeyAuth": [] } ], - "description": "user's vote", + "description": "get user personal votes", "consumes": [ "application/json" ], @@ -3442,7 +3524,7 @@ "tags": [ "Activity" ], - "summary": "user's votes", + "summary": "get user personal votes", "parameters": [ { "type": "integer", @@ -6333,7 +6415,8 @@ "properties": { "display_name": { "type": "string", - "maxLength": 30 + "maxLength": 30, + "minLength": 4 }, "email": { "type": "string", @@ -7070,7 +7153,7 @@ } }, "selected_level": { - "type": "integer" + "$ref": "#/definitions/schema.PrivilegeLevel" } } }, @@ -7385,6 +7468,14 @@ } } }, + "schema.GetUserActivationResp": { + "type": "object", + "properties": { + "activation_url": { + "type": "string" + } + } + }, "schema.GetUserPageResp": { "type": "object", "properties": { @@ -7561,11 +7652,24 @@ } } }, + "schema.PrivilegeLevel": { + "type": "integer", + "enum": [ + 1, + 2, + 3 + ], + "x-enum-varnames": [ + "PrivilegeLevel1", + "PrivilegeLevel2", + "PrivilegeLevel3" + ] + }, "schema.PrivilegeOption": { "type": "object", "properties": { "level": { - "type": "integer" + "$ref": "#/definitions/schema.PrivilegeLevel" }, "level_desc": { "type": "string" @@ -7651,11 +7755,11 @@ "schema.QuestionPageReq": { "type": "object", "properties": { - "inDays": { + "in_days": { "type": "integer", "minimum": 1 }, - "orderCond": { + "order": { "type": "string", "enum": [ "newest", @@ -7669,7 +7773,7 @@ "type": "integer", "minimum": 1 }, - "pageSize": { + "page_size": { "type": "integer", "minimum": 1 }, @@ -7980,7 +8084,11 @@ }, "user_info": { "description": "user info", - "$ref": "#/definitions/schema.UserBasicInfo" + "allOf": [ + { + "$ref": "#/definitions/schema.UserBasicInfo" + } + ] }, "vote_count": { "type": "integer" @@ -7992,7 +8100,11 @@ "properties": { "object": { "description": "this object", - "$ref": "#/definitions/schema.SearchObject" + "allOf": [ + { + "$ref": "#/definitions/schema.SearchObject" + } + ] }, "object_type": { "description": "object_type", @@ -8000,6 +8112,17 @@ } } }, + "schema.SendUserActivationReq": { + "type": "object", + "required": [ + "user_id" + ], + "properties": { + "user_id": { + "type": "string" + } + } + }, "schema.SiteBrandingReq": { "type": "object", "properties": { @@ -8174,7 +8297,7 @@ "type": "string" }, "site_seo": { - "$ref": "#/definitions/schema.SiteSeoReq" + "$ref": "#/definitions/schema.SiteSeoResp" }, "site_users": { "$ref": "#/definitions/schema.SiteUsersResp" @@ -8623,7 +8746,11 @@ "properties": { "avatar": { "description": "avatar", - "$ref": "#/definitions/schema.AvatarInfo" + "allOf": [ + { + "$ref": "#/definitions/schema.AvatarInfo" + } + ] }, "bio": { "description": "bio", @@ -8690,9 +8817,13 @@ ], "properties": { "level": { - "type": "integer", "maximum": 3, - "minimum": 1 + "minimum": 1, + "allOf": [ + { + "$ref": "#/definitions/schema.PrivilegeLevel" + } + ] } } }, diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 4f73fe51..5781165d 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -206,6 +206,7 @@ definitions: properties: display_name: maxLength: 30 + minLength: 4 type: string email: maxLength: 500 @@ -729,7 +730,7 @@ definitions: $ref: '#/definitions/schema.PrivilegeOption' type: array selected_level: - type: integer + $ref: '#/definitions/schema.PrivilegeLevel' type: object schema.GetRankPersonalPageResp: properties: @@ -952,6 +953,11 @@ definitions: unreviewed_info: $ref: '#/definitions/schema.GetRevisionResp' type: object + schema.GetUserActivationResp: + properties: + activation_url: + type: string + type: object schema.GetUserPageResp: properties: avatar: @@ -1075,10 +1081,20 @@ definitions: content: type: string type: object + schema.PrivilegeLevel: + enum: + - 1 + - 2 + - 3 + type: integer + x-enum-varnames: + - PrivilegeLevel1 + - PrivilegeLevel2 + - PrivilegeLevel3 schema.PrivilegeOption: properties: level: - type: integer + $ref: '#/definitions/schema.PrivilegeLevel' level_desc: type: string privileges: @@ -1141,10 +1157,10 @@ definitions: type: object schema.QuestionPageReq: properties: - inDays: + in_days: minimum: 1 type: integer - orderCond: + order: enum: - newest - active @@ -1155,7 +1171,7 @@ definitions: page: minimum: 1 type: integer - pageSize: + page_size: minimum: 1 type: integer tag: @@ -1368,7 +1384,8 @@ definitions: title: type: string user_info: - $ref: '#/definitions/schema.UserBasicInfo' + allOf: + - $ref: '#/definitions/schema.UserBasicInfo' description: user info vote_count: type: integer @@ -1376,12 +1393,20 @@ definitions: schema.SearchResp: properties: object: - $ref: '#/definitions/schema.SearchObject' + allOf: + - $ref: '#/definitions/schema.SearchObject' description: this object object_type: description: object_type type: string type: object + schema.SendUserActivationReq: + properties: + user_id: + type: string + required: + - user_id + type: object schema.SiteBrandingReq: properties: favicon: @@ -1507,7 +1532,7 @@ definitions: revision: type: string site_seo: - $ref: '#/definitions/schema.SiteSeoReq' + $ref: '#/definitions/schema.SiteSeoResp' site_users: $ref: '#/definitions/schema.SiteUsersResp' theme: @@ -1811,7 +1836,8 @@ definitions: schema.UpdateInfoRequest: properties: avatar: - $ref: '#/definitions/schema.AvatarInfo' + allOf: + - $ref: '#/definitions/schema.AvatarInfo' description: avatar bio: description: bio @@ -1858,9 +1884,10 @@ definitions: schema.UpdatePrivilegesConfigReq: properties: level: + allOf: + - $ref: '#/definitions/schema.PrivilegeLevel' maximum: 3 minimum: 1 - type: integer required: - level type: object @@ -2335,7 +2362,7 @@ paths: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] - summary: AdminSearchAnswerList + summary: AdminAnswerPage admin answer page tags: - admin /answer/admin/api/answer/status: @@ -2533,7 +2560,7 @@ paths: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] - summary: AdminSearchList + summary: AdminQuestionPage admin question page tags: - admin /answer/admin/api/question/status: @@ -3219,6 +3246,32 @@ paths: summary: add user tags: - admin + /answer/admin/api/user/activation: + get: + description: get user activation + parameters: + - description: user id + in: query + name: user_id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/handler.RespBody' + - properties: + data: + $ref: '#/definitions/schema.GetUserActivationResp' + type: object + security: + - ApiKeyAuth: [] + summary: get user activation + tags: + - admin /answer/admin/api/user/password: put: consumes: @@ -3291,6 +3344,28 @@ paths: summary: update user tags: - admin + /answer/admin/api/users/activation: + post: + description: send user activation + parameters: + - description: SendUserActivationReq + in: body + name: data + required: true + schema: + $ref: '#/definitions/schema.SendUserActivationReq' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handler.RespBody' + security: + - ApiKeyAuth: [] + summary: send user activation + tags: + - admin /answer/admin/api/users/page: get: description: get user page @@ -4364,7 +4439,7 @@ paths: get: consumes: - application/json - description: user's vote + description: get user personal votes parameters: - description: page size in: query @@ -4395,7 +4470,7 @@ paths: type: object security: - ApiKeyAuth: [] - summary: user's votes + summary: get user personal votes tags: - Activity /answer/api/v1/post/render: diff --git a/go.mod b/go.mod index 1e861790..d19dcb24 100644 --- a/go.mod +++ b/go.mod @@ -10,12 +10,12 @@ require ( 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 - github.com/go-playground/universal-translator v0.18.0 - github.com/go-playground/validator/v10 v10.11.1 + github.com/gin-gonic/gin v1.9.1 + github.com/go-playground/locales v0.14.1 + github.com/go-playground/universal-translator v0.18.1 + github.com/go-playground/validator/v10 v10.14.0 github.com/go-sql-driver/mysql v1.6.0 - github.com/goccy/go-json v0.9.11 + github.com/goccy/go-json v0.10.2 github.com/golang/mock v1.6.0 github.com/google/uuid v1.3.0 github.com/google/wire v0.5.0 @@ -36,19 +36,18 @@ require ( 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 - github.com/stretchr/testify v1.8.1 + github.com/stretchr/testify v1.8.4 github.com/swaggo/files v1.0.0 github.com/swaggo/gin-swagger v1.5.3 - github.com/swaggo/swag v1.8.10 + github.com/swaggo/swag v1.16.1 github.com/tidwall/gjson v1.14.4 github.com/yuin/goldmark v1.4.13 - golang.org/x/crypto v0.1.0 - golang.org/x/net v0.7.0 + golang.org/x/crypto v0.11.0 + golang.org/x/net v0.12.0 gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df gopkg.in/yaml.v3 v3.0.1 - modernc.org/sqlite v1.14.2 + modernc.org/sqlite v1.24.0 xorm.io/builder v0.3.12 - xorm.io/core v0.7.3 xorm.io/xorm v1.3.2 ) @@ -60,7 +59,9 @@ require ( github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect github.com/andybalholm/brotli v1.0.4 // indirect github.com/aymerick/douceur v0.2.0 // indirect + github.com/bytedance/sonic v1.9.1 // indirect github.com/cenkalti/backoff/v4 v4.1.3 // indirect + github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect github.com/containerd/continuity v0.3.0 // indirect github.com/docker/cli v20.10.14+incompatible // indirect github.com/docker/docker v20.10.7+incompatible // indirect @@ -70,18 +71,19 @@ require ( github.com/dsoprea/go-jpeg-image-structure v0.0.0-20190422055009-d6f9ba25cf48 // indirect github.com/dsoprea/go-logging v0.0.0-20190624164917-c4f10aab7696 // indirect github.com/dsoprea/go-png-image-structure v0.0.0-20190624104353-c9b28dcdc5c8 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.2 // indirect github.com/gin-contrib/sse v0.1.0 // indirect github.com/go-errors/errors v1.0.1 // indirect - github.com/go-openapi/jsonpointer v0.19.5 // indirect - github.com/go-openapi/jsonreference v0.20.0 // indirect - github.com/go-openapi/spec v0.20.7 // indirect - github.com/go-openapi/swag v0.22.3 // indirect + github.com/go-openapi/jsonpointer v0.20.0 // indirect + github.com/go-openapi/jsonreference v0.20.2 // indirect + github.com/go-openapi/spec v0.20.9 // indirect + github.com/go-openapi/swag v0.22.4 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect github.com/golang/geo v0.0.0-20190812012225-f41920e961ce // indirect github.com/golang/snappy v0.0.4 // indirect - github.com/google/go-cmp v0.5.9 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/gorilla/css v1.0.0 // indirect github.com/gosimple/unidecode v1.0.1 // indirect @@ -91,13 +93,13 @@ require ( github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect - github.com/leodido/go-urn v1.2.1 // indirect + github.com/klauspost/cpuid/v2 v2.2.4 // indirect + github.com/leodido/go-urn v1.2.4 // indirect github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible // indirect github.com/lestrrat-go/strftime v1.0.6 // indirect github.com/magiconair/properties v1.8.6 // indirect github.com/mailru/easyjson v0.7.7 // indirect - github.com/mattn/go-isatty v0.0.16 // indirect - github.com/mattn/go-sqlite3 v1.14.16 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/moby/term v0.0.0-20201216013528-df9cb8a40635 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect @@ -107,10 +109,10 @@ require ( github.com/opencontainers/runc v1.1.2 // indirect github.com/patrickmn/go-cache v2.1.0+incompatible // indirect github.com/pelletier/go-toml v1.9.5 // indirect - github.com/pelletier/go-toml/v2 v2.0.5 // indirect + github.com/pelletier/go-toml/v2 v2.0.8 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/sirupsen/logrus v1.8.1 // indirect github.com/spf13/afero v1.9.2 // indirect github.com/spf13/cast v1.5.0 // indirect @@ -121,32 +123,36 @@ require ( github.com/syndtr/goleveldb v1.0.0 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.0 // indirect - github.com/ugorji/go/codec v1.2.7 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.11 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xeipuuv/gojsonschema v1.2.0 // indirect go.uber.org/atomic v1.10.0 // indirect go.uber.org/multierr v1.8.0 // indirect go.uber.org/zap v1.23.0 // indirect + golang.org/x/arch v0.3.0 // indirect golang.org/x/image v0.1.0 // indirect - golang.org/x/mod v0.8.0 // indirect - golang.org/x/sys v0.5.0 // indirect - golang.org/x/text v0.9.0 // indirect - golang.org/x/tools v0.6.0 // indirect - google.golang.org/protobuf v1.28.1 // indirect + golang.org/x/mod v0.12.0 // indirect + golang.org/x/sys v0.10.0 // indirect + golang.org/x/text v0.11.0 // indirect + golang.org/x/tools v0.11.0 // indirect + google.golang.org/protobuf v1.30.0 // indirect gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect - lukechampine.com/uint128 v1.1.1 // indirect - modernc.org/cc/v3 v3.35.18 // indirect - modernc.org/ccgo/v3 v3.12.82 // indirect - modernc.org/libc v1.11.87 // indirect - modernc.org/mathutil v1.4.1 // indirect - modernc.org/memory v1.0.5 // indirect - modernc.org/opt v0.1.1 // indirect - modernc.org/strutil v1.1.1 // indirect - modernc.org/token v1.0.0 // indirect + lukechampine.com/uint128 v1.2.0 // indirect + modernc.org/cc/v3 v3.40.0 // indirect + modernc.org/ccgo/v3 v3.16.13 // indirect + modernc.org/libc v1.22.5 // indirect + modernc.org/mathutil v1.5.0 // indirect + modernc.org/memory v1.5.0 // indirect + modernc.org/opt v0.1.3 // indirect + modernc.org/strutil v1.1.3 // indirect + modernc.org/token v1.0.1 // indirect sigs.k8s.io/yaml v1.3.0 // indirect ) replace lukechampine.com/uint128 v1.1.1 => github.com/aichy126/uint128 v1.1.1 + +replace modernc.org/cc/v3 v3.40.0 => gitlab.com/cznic/cc/v3 v3.40.0 diff --git a/go.sum b/go.sum index e26ab91c..6124c0a0 100644 --- a/go.sum +++ b/go.sum @@ -65,7 +65,6 @@ github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMx github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= github.com/agiledragon/gomonkey/v2 v2.3.1/go.mod h1:ap1AmDzcVOAz1YpeJ3TCzIgstoaWLA6jbbgxfB4w2iY= -github.com/aichy126/uint128 v1.1.1 h1:xH1bCWDzq7Ebm4lpXCeIiWco0VWi7UmiKkvTQSWBmb0= github.com/aichy126/uint128 v1.1.1/go.mod h1:Hke/MPGXUxOl0OXHoNcVesBL4N+XalHEJ9e1jaIbl8o= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= @@ -96,6 +95,9 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bwmarrin/snowflake v0.3.0 h1:xm67bEhkKh6ij1790JB83OujPR5CzNe8QuQqAgISZN0= github.com/bwmarrin/snowflake v0.3.0/go.mod h1:NdZxfVWX+oR6y2K0o6qAYv6gIOP9rjG0/E9WsDpxqwE= +github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= +github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s= +github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ= github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= github.com/cenkalti/backoff/v4 v4.1.3 h1:cFAlzYUlVYDysBEH2T5hyJZMh3+5+WCBvSnK6Q8UtC4= @@ -103,6 +105,9 @@ github.com/cenkalti/backoff/v4 v4.1.3/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInq github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/checkpoint-restore/go-criu/v5 v5.3.0/go.mod h1:E/eQpaFtUKGOOSEBZgmKAcn+zUUwWxqcaKZlF54wK8E= +github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= +github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= +github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= @@ -155,8 +160,9 @@ github.com/dsoprea/go-logging v0.0.0-20190624164917-c4f10aab7696/go.mod h1:Nm/x2 github.com/dsoprea/go-png-image-structure v0.0.0-20190624104353-c9b28dcdc5c8 h1:SVQfy5rBFZXzvGkU2MZ0RzpS912/1sJrEJ+FMmeaC9U= github.com/dsoprea/go-png-image-structure v0.0.0-20190624104353-c9b28dcdc5c8/go.mod h1:Bf0nmcDFFRQBjZwr9qY6c0zTxKQa+Q8YWZmlYxXGxY0= github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= -github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= @@ -176,14 +182,17 @@ github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3 github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= +github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= +github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4= github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-gonic/gin v1.7.0/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY= -github.com/gin-gonic/gin v1.8.1 h1:4+fr/el88TOO3ewCmQr8cx/CtZ/umlIRIs5M4NTNjf8= github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk= +github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= +github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w= github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= @@ -196,39 +205,44 @@ github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9 github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= -github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-openapi/jsonpointer v0.20.0 h1:ESKJdU9ASRfaPNOPRx12IUyA1vn3R9GiE3KYD14BXdQ= +github.com/go-openapi/jsonpointer v0.20.0/go.mod h1:6PGzBjjIIumbLYysB73Klnms1mwnU4G3YHOECG3CedA= github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns= -github.com/go-openapi/jsonreference v0.20.0 h1:MYlu0sBgChmCfJxxUKZ8g1cPWFOB37YSZqewK7OKeyA= github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo= +github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= +github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I= -github.com/go-openapi/spec v0.20.7 h1:1Rlu/ZrOCCob0n+JKKJAWhNWMPW8bOZRg8FJaY+0SKI= -github.com/go-openapi/spec v0.20.7/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA= +github.com/go-openapi/spec v0.20.9 h1:xnlYNQAwKd2VQRRfwTEI0DcK+2cbuvI/0c7jx3gA8/8= +github.com/go-openapi/spec v0.20.9/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA= github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= -github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= -github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= +github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogBU= +github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= -github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU= github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= -github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho= github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos= -github.com/go-playground/validator/v10 v10.11.1 h1:prmOlTVv+YjZjmRmNSF3VmspqJIxJWXmqUsHwfTRRkQ= -github.com/go-playground/validator/v10 v10.11.1/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU= +github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js= +github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= -github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/goccy/go-json v0.8.1/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= -github.com/goccy/go-json v0.9.11 h1:/pAaQDLHEoCq/5FFmSKBswWmK6H0e8g4159Kc/X/nqk= -github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.0.6/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= @@ -291,7 +305,6 @@ github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= @@ -306,6 +319,7 @@ github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hf github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ= 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= @@ -439,22 +453,26 @@ github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:C github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= +github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= -github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= +github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= +github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= github.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc h1:RKf14vYWi2ttpEmkA4aQ3j4u9dStX2t4M8UM6qqNsG8= github.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc/go.mod h1:kopuH9ugFRkIXf3YoqHKyrJ9YfUFsckUU9S7B+XP+is= github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible h1:Y6sqxHMyB1D2YSzWkLibYKgg+SwmyFU9dF2hn6MdTj4= @@ -490,13 +508,11 @@ github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= -github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= -github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mattn/go-sqlite3 v1.14.9/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= -github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/microcosm-cc/bluemonday v1.0.21 h1:dNH3e4PSyE4vNX+KlRGHT5KrSvjeUkoNPwEORjffHJg= github.com/microcosm-cc/bluemonday v1.0.21/go.mod h1:ytNkv4RrDrLJ2pqlsSI46O6IVXmZOBBD4SaJyDwwTkM= @@ -572,8 +588,8 @@ github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtP github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo= -github.com/pelletier/go-toml/v2 v2.0.5 h1:ipoSadvV8oGUjnUbMub59IDPPwfxF694nG/jwbMiyQg= -github.com/pelletier/go-toml/v2 v2.0.5/go.mod h1:OMHamSCAODeSsVrwwvcJOaoN0LIUIaFVNZzmWyNfXas= +github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= +github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac= github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc= github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= @@ -604,8 +620,9 @@ github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= 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/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/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= @@ -682,8 +699,11 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/subosito/gotenv v1.4.1 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs= github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= github.com/swaggo/files v0.0.0-20220728132757-551d4a08d97a/go.mod h1:lKJPbtWzJ9JhsTN1k1gZgleJWY/cqq0psdoMmaThG3w= @@ -692,8 +712,8 @@ github.com/swaggo/files v1.0.0/go.mod h1:N59U6URJLyU1PQgFqPM7wXLMhJx7QAolnvfQkqO github.com/swaggo/gin-swagger v1.5.3 h1:8mWmHLolIbrhJJTflsaFoZzRBYVmEE7JZGIq08EiC0Q= github.com/swaggo/gin-swagger v1.5.3/go.mod h1:3XJKSfHjDMB5dBo/0rrTXidPmgLeqsX89Yp4uA50HpI= github.com/swaggo/swag v1.8.1/go.mod h1:ugemnJsPZm/kRwFUnzBlbHRd0JY9zE1M4F+uy2pAaPQ= -github.com/swaggo/swag v1.8.10 h1:eExW4bFa52WOjqRzRD58bgWsWfdFJso50lpbeTcmTfo= -github.com/swaggo/swag v1.8.10/go.mod h1:ezQVUUhly8dludpVk+/PuwJWvLLanB13ygV5Pr9enSk= +github.com/swaggo/swag v1.16.1 h1:fTNRhKstPKxcnoKsytm4sahr8FaYzUcT7i1/3nd/fBg= +github.com/swaggo/swag v1.16.1/go.mod h1:9/LMvHycG3NFHfR6LwvikHv5iFvmPADQ359cKikGxto= github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE= github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ= @@ -704,11 +724,14 @@ github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JT github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M= github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= -github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0= github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= +github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= +github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= @@ -731,6 +754,8 @@ github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0= +gitlab.com/cznic/cc/v3 v3.40.0 h1:3yn6qVCeHjTSJio35QIhNsD5POKm7ETHXS54yKqzDEo= +gitlab.com/cznic/cc/v3 v3.40.0/go.mod h1:/bTg4dnWkSXowUO6ssQKnOV0yMVxDYNIsIrzqTFDGH0= go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg= go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= @@ -760,6 +785,9 @@ go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= go.uber.org/zap v1.23.0 h1:OjGQ5KQDEUawVHxNwQgPpiypGHOxo2mNZsOqTak4fFY= go.uber.org/zap v1.23.0/go.mod h1:D+nX8jyLsMHMYrln8A0rJjFt/T/9/bGgIhAqxv5URuY= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= +golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -780,9 +808,8 @@ golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU= -golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= +golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA= +golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -822,8 +849,8 @@ golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 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.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 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= @@ -866,12 +893,12 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= -golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50= +golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -893,7 +920,7 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= +golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -962,12 +989,14 @@ golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA= +golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -983,8 +1012,8 @@ 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/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4= +golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 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= @@ -1052,8 +1081,8 @@ golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.11.0 h1:EMCa6U9S2LtZXLAMoWiR/R8dAQFRqbAitmbJ2UKhoi8= +golang.org/x/tools v0.11.0/go.mod h1:anzJrxPjNtfgiYQYirP2CPGzGLxrH2u2QBhn6Bf3qY8= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -1084,7 +1113,6 @@ google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9Ywl google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= @@ -1160,8 +1188,8 @@ google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlba google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= -google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= +google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk= gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk= @@ -1208,6 +1236,8 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +lukechampine.com/uint128 v1.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI= +lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= modernc.org/cc/v3 v3.33.6/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= modernc.org/cc/v3 v3.33.9/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= modernc.org/cc/v3 v3.33.11/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= @@ -1221,7 +1251,6 @@ modernc.org/cc/v3 v3.35.10/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g modernc.org/cc/v3 v3.35.15/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= modernc.org/cc/v3 v3.35.16/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= modernc.org/cc/v3 v3.35.17/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= -modernc.org/cc/v3 v3.35.18 h1:rMZhRcWrba0y3nVmdiQ7kxAgOOSq2m2f2VzjHLgEs6U= modernc.org/cc/v3 v3.35.18/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= modernc.org/ccgo/v3 v3.9.5/go.mod h1:umuo2EP2oDSBnD3ckjaVUXMrmeAw8C8OSICVa0iFf60= modernc.org/ccgo/v3 v3.10.0/go.mod h1:c0yBmkRFi7uW4J7fwx/JiijwOjeAeR2NoSaRVFPmjMw= @@ -1257,10 +1286,11 @@ modernc.org/ccgo/v3 v3.12.66/go.mod h1:jUuxlCFZTUZLMV08s7B1ekHX5+LIAurKTTaugUr/E modernc.org/ccgo/v3 v3.12.67/go.mod h1:Bll3KwKvGROizP2Xj17GEGOTrlvB1XcVaBrC90ORO84= modernc.org/ccgo/v3 v3.12.73/go.mod h1:hngkB+nUUqzOf3iqsM48Gf1FZhY599qzVg1iX+BT3cQ= modernc.org/ccgo/v3 v3.12.81/go.mod h1:p2A1duHoBBg1mFtYvnhAnQyI6vL0uw5PGYLSIgF6rYY= -modernc.org/ccgo/v3 v3.12.82 h1:wudcnJyjLj1aQQCXF3IM9Gz2X6UNjw+afIghzdtn0v8= modernc.org/ccgo/v3 v3.12.82/go.mod h1:ApbflUfa5BKadjHynCficldU1ghjen84tuM5jRynB7w= -modernc.org/ccorpus v1.11.1 h1:K0qPfpVG1MJh5BYazccnmhywH4zHuOgJXgbjzyp6dWA= +modernc.org/ccgo/v3 v3.16.13 h1:Mkgdzl46i5F/CNR/Kj80Ri59hC8TKAhZrYSaqvkwzUw= +modernc.org/ccgo/v3 v3.16.13/go.mod h1:2Quk+5YgpImhPjv2Qsob1DnZ/4som1lJTodubIcoUkY= modernc.org/ccorpus v1.11.1/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ= +modernc.org/ccorpus v1.11.6 h1:J16RXiiqiCgua6+ZvQot4yUuUy8zxgqbqEEUuGPlISk= modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM= modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM= modernc.org/libc v1.9.8/go.mod h1:U1eq8YWr/Kc1RWCMFUWEdkTg8OTcfLw2kY8EDwl039w= @@ -1297,29 +1327,37 @@ modernc.org/libc v1.11.71/go.mod h1:DUOmMYe+IvKi9n6Mycyx3DbjfzSKrdr/0Vgt3j7P5gw= modernc.org/libc v1.11.75/go.mod h1:dGRVugT6edz361wmD9gk6ax1AbDSe0x5vji0dGJiPT0= modernc.org/libc v1.11.82/go.mod h1:NF+Ek1BOl2jeC7lw3a7Jj5PWyHPwWD4aq3wVKxqV1fI= modernc.org/libc v1.11.86/go.mod h1:ePuYgoQLmvxdNT06RpGnaDKJmDNEkV7ZPKI2jnsvZoE= -modernc.org/libc v1.11.87 h1:PzIzOqtlzMDDcCzJ5cUP6h/Ku6Fa9iyflP2ccTY64aE= modernc.org/libc v1.11.87/go.mod h1:Qvd5iXTeLhI5PS0XSyqMY99282y+3euapQFxM7jYnpY= +modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE= +modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY= modernc.org/mathutil v1.1.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= modernc.org/mathutil v1.2.2/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= modernc.org/mathutil v1.4.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= -modernc.org/mathutil v1.4.1 h1:ij3fYGe8zBF4Vu+g0oT7mB06r8sqGWKuJu1yXeR4by8= modernc.org/mathutil v1.4.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ= +modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= modernc.org/memory v1.0.4/go.mod h1:nV2OApxradM3/OVbs2/0OsP6nPfakXpi50C7dcoHXlc= -modernc.org/memory v1.0.5 h1:XRch8trV7GgvTec2i7jc33YlUI0RKVDBvZ5eZ5m8y14= modernc.org/memory v1.0.5/go.mod h1:B7OYswTRnfGg+4tDH1t1OeUNnsy2viGTdME4tzd+IjM= -modernc.org/opt v0.1.1 h1:/0RX92k9vwVeDXj+Xn23DKp2VJubL7k8qNffND6qn3A= +modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds= +modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= modernc.org/opt v0.1.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= -modernc.org/sqlite v1.14.2 h1:ohsW2+e+Qe2To1W6GNezzKGwjXwSax6R+CrhRxVaFbE= +modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4= +modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= modernc.org/sqlite v1.14.2/go.mod h1:yqfn85u8wVOE6ub5UT8VI9JjhrwBUUCNyTACN0h6Sx8= -modernc.org/strutil v1.1.1 h1:xv+J1BXY3Opl2ALrBwyfEikFAj8pmqcpnfmuwUwcozs= +modernc.org/sqlite v1.24.0 h1:EsClRIWHGhLTCX44p+Ri/JLD+vFGo0QGjasg2/F9TlI= +modernc.org/sqlite v1.24.0/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk= modernc.org/strutil v1.1.1/go.mod h1:DE+MQQ/hjKBZS2zNInV5hhcipt5rLPWkmpbGeW5mmdw= -modernc.org/tcl v1.8.13 h1:V0sTNBw0Re86PvXZxuCub3oO9WrSTqALgrwNZNvLFGw= +modernc.org/strutil v1.1.3 h1:fNMm+oJklMGYfU9Ylcywl0CO5O6nTfaowNsh2wpPjzY= +modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw= modernc.org/tcl v1.8.13/go.mod h1:V+q/Ef0IJaNUSECieLU4o+8IScapxnMyFV6i/7uQlAY= -modernc.org/token v1.0.0 h1:a0jaWiNMDhDUtqOj09wvjWWAqd3q7WpBulmL9H2egsk= +modernc.org/tcl v1.15.2 h1:C4ybAYCGJw968e+Me18oW55kD/FexcHbqH2xak1ROSY= modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= -modernc.org/z v1.2.19 h1:BGyRFWhDVn5LFS5OcX4Yd/MlpRTOc7hOPTdcIpCiUao= +modernc.org/token v1.0.1 h1:A3qvTqOwexpfZZeyI0FeGPDlSWX5pjZu9hF4lU+EKWg= +modernc.org/token v1.0.1/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= modernc.org/z v1.2.19/go.mod h1:+ZpP0pc4zz97eukOzW3xagV/lS82IpPN9NGG5pNF9vY= +modernc.org/z v1.7.3 h1:zDJf6iHjrnB+WRD88stbXokugjyc0/pB91ri1gO6LZY= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= @@ -1329,7 +1367,5 @@ sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1 xorm.io/builder v0.3.11-0.20220531020008-1bd24a7dc978/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE= xorm.io/builder v0.3.12 h1:ASZYX7fQmy+o8UJdhlLHSW57JDOkM8DNhcAF5d0LiJM= xorm.io/builder v0.3.12/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE= -xorm.io/core v0.7.3 h1:W8ws1PlrnkS1CZU1YWaYLMQcQilwAmQXU0BJDJon+H0= -xorm.io/core v0.7.3/go.mod h1:jJfd0UAEzZ4t87nbQYtVjmqpIODugN6PD2D9E+dJvdM= xorm.io/xorm v1.3.2 h1:uTRRKF2jYzbZ5nsofXVUx6ncMaek+SHjWYtCXyZo1oM= xorm.io/xorm v1.3.2/go.mod h1:9NbjqdnjX6eyjRRhh01GHm64r6N9shTb/8Ak3YRt8Nw= diff --git a/i18n/en_US.yaml b/i18n/en_US.yaml index e6abbd80..c865fc08 100644 --- a/i18n/en_US.yaml +++ b/i18n/en_US.yaml @@ -108,8 +108,12 @@ backend: other: Manage tag synonyms email: other: Email + e_mail: + other: Email password: other: Password + pass: + other: Password email_or_password_wrong_error: other: Email and password do not match. error: @@ -504,6 +508,7 @@ ui: title: Notifications inbox: Inbox achievement: Achievements + new_alerts: New alerts all_read: Mark all as read show_more: Show more someone: Someone @@ -729,7 +734,7 @@ ui: tip_answer: >- Use comments to reply to other users or notify them of changes. If you are adding new information, edit your post instead of commenting. - tip_vote: It adds something useful to the post + tip_vote: It is useful edit_answer: title: Edit Answer default_reason: Edit answer @@ -841,6 +846,9 @@ ui: change_btn_name: Change email msg: empty: Cannot be empty. + resend_email: + url_label: Are you sure you want to resend the activation email? + url_text: You can also give the activation link above to the user. login: login_to_continue: Log in to continue info_sign: Don't have an account? <1>Sign up @@ -940,7 +948,6 @@ ui: gravatar: Gravatar gravatar_text: You can change image on custom: Custom - btn_refresh: Refresh custom_text: You can upload your image. default: System msg: Please upload an avatar @@ -1000,9 +1007,9 @@ ui: flag_success: Thanks for flagging. forbidden_operate_self: Forbidden to operate on yourself review: Your revision will show after review. + sent_success: Sent successfully related_question: title: Related Questions - btn: Add question answers: answers invite_to_answer: title: People asked @@ -1026,6 +1033,7 @@ ui: useful: Useful question_useful: It is useful and clear question_un_useful: It is unclear or not useful + question_bookmark: Bookmark this question answer_useful: It is useful answer_un_useful: It is not useful answers: @@ -1093,6 +1101,8 @@ ui: question: Question answer: Answer comment: Comment + refresh: Refresh + resend: Resend search: title: Search Results keywords: Keywords @@ -1404,6 +1414,31 @@ ui: title: Change user role to... btn_cancel: Cancel btn_submit: Submit + 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 users: title: Users name: Name @@ -1433,33 +1468,6 @@ ui: 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 @@ -1628,7 +1636,7 @@ ui: text: This will insert after footer: label: Footer - text: This will insert before . + text: This will insert before . sidebar: label: Sidebar text: This will insert in sidebar. diff --git a/i18n/zh_CN.yaml b/i18n/zh_CN.yaml index b97709f3..f17af277 100644 --- a/i18n/zh_CN.yaml +++ b/i18n/zh_CN.yaml @@ -107,8 +107,12 @@ backend: other: 管理标签同义词 email: other: 邮箱 + e_mail: + other: 邮箱 password: other: 密码 + pass: + other: 密码 email_or_password_wrong_error: other: 邮箱和密码不匹配。 error: diff --git a/internal/base/constant/cache_key.go b/internal/base/constant/cache_key.go index 9e9f9949..c4788dac 100644 --- a/internal/base/constant/cache_key.go +++ b/internal/base/constant/cache_key.go @@ -16,4 +16,7 @@ const ( ConfigKEY2ContentCacheKeyPrefix = "answer:config:key:" ConnectorUserExternalInfoCacheKey = "answer:connector:" ConnectorUserExternalInfoCacheTime = 10 * time.Minute + SiteMapQuestionCacheKeyPrefix = "answer:sitemap:question:%d" + SiteMapQuestionCacheTime = time.Hour + SitemapMaxSize = 50000 ) diff --git a/internal/base/constant/http_header.go b/internal/base/constant/ctx_flag.go similarity index 62% rename from internal/base/constant/http_header.go rename to internal/base/constant/ctx_flag.go index a68db8b1..822c1a01 100644 --- a/internal/base/constant/http_header.go +++ b/internal/base/constant/ctx_flag.go @@ -2,4 +2,5 @@ package constant const ( AcceptLanguageFlag = "Accept-Language" + ShortIDFlag = "Short-ID-Enabled" ) diff --git a/internal/base/constant/site_info.go b/internal/base/constant/site_info.go index dfcee76c..d3c4e52a 100644 --- a/internal/base/constant/site_info.go +++ b/internal/base/constant/site_info.go @@ -7,3 +7,14 @@ const ( AvatarTypeGravatar = "gravatar" AvatarTypeCustom = "custom" ) + +const ( + // PermaLinkQuestionIDAndTitle /questions/10010000000000001/post-title + PermaLinkQuestionIDAndTitle = iota + 1 + // PermaLinkQuestionID /questions/10010000000000001 + PermaLinkQuestionID + // PermaLinkQuestionIDAndTitleByShortID /questions/11/post-title + PermaLinkQuestionIDAndTitleByShortID + // PermaLinkQuestionIDByShortID /questions/11 + PermaLinkQuestionIDByShortID +) diff --git a/internal/base/cron/cron.go b/internal/base/cron/cron.go index 79c34597..04f396b0 100644 --- a/internal/base/cron/cron.go +++ b/internal/base/cron/cron.go @@ -12,13 +12,13 @@ import ( // ScheduledTaskManager scheduled task manager type ScheduledTaskManager struct { - siteInfoService *siteinfo_common.SiteInfoCommonService + siteInfoService siteinfo_common.SiteInfoCommonService questionService *service.QuestionService } // NewScheduledTaskManager new scheduled task manager func NewScheduledTaskManager( - siteInfoService *siteinfo_common.SiteInfoCommonService, + siteInfoService siteinfo_common.SiteInfoCommonService, questionService *service.QuestionService, ) *ScheduledTaskManager { manager := &ScheduledTaskManager{ diff --git a/internal/base/data/data.go b/internal/base/data/data.go index 113d8d48..e8a32637 100644 --- a/internal/base/data/data.go +++ b/internal/base/data/data.go @@ -12,9 +12,9 @@ import ( "github.com/segmentfault/pacman/contrib/cache/memory" "github.com/segmentfault/pacman/log" _ "modernc.org/sqlite" - "xorm.io/core" "xorm.io/xorm" ormlog "xorm.io/xorm/log" + "xorm.io/xorm/names" "xorm.io/xorm/schemas" ) @@ -71,7 +71,7 @@ func NewDB(debug bool, dataConf *Database) (*xorm.Engine, error) { if dataConf.ConnMaxLifeTime > 0 { engine.SetConnMaxLifetime(time.Duration(dataConf.ConnMaxLifeTime) * time.Second) } - engine.SetColumnMapper(core.GonicMapper{}) + engine.SetColumnMapper(names.GonicMapper{}) return engine, nil } diff --git a/internal/base/handler/short_id.go b/internal/base/handler/short_id.go new file mode 100644 index 00000000..35d62e62 --- /dev/null +++ b/internal/base/handler/short_id.go @@ -0,0 +1,16 @@ +package handler + +import ( + "context" + + "github.com/answerdev/answer/internal/base/constant" +) + +// GetEnableShortID get language from header +func GetEnableShortID(ctx context.Context) bool { + flag, ok := ctx.Value(constant.ShortIDFlag).(bool) + if ok { + return flag + } + return false +} diff --git a/internal/base/middleware/auth.go b/internal/base/middleware/auth.go index ed941c0f..5532a479 100644 --- a/internal/base/middleware/auth.go +++ b/internal/base/middleware/auth.go @@ -1,19 +1,22 @@ package middleware import ( + "net/http" "strings" "github.com/answerdev/answer/internal/schema" "github.com/answerdev/answer/internal/service/role" "github.com/answerdev/answer/internal/service/siteinfo_common" + "github.com/answerdev/answer/ui" + "github.com/gin-gonic/gin" "github.com/answerdev/answer/internal/base/handler" "github.com/answerdev/answer/internal/base/reason" "github.com/answerdev/answer/internal/entity" "github.com/answerdev/answer/internal/service/auth" "github.com/answerdev/answer/pkg/converter" - "github.com/gin-gonic/gin" "github.com/segmentfault/pacman/errors" + "github.com/segmentfault/pacman/log" ) var ctxUUIDKey = "ctxUuidKey" @@ -21,13 +24,13 @@ var ctxUUIDKey = "ctxUuidKey" // AuthUserMiddleware auth user middleware type AuthUserMiddleware struct { authService *auth.AuthService - siteInfoCommonService *siteinfo_common.SiteInfoCommonService + siteInfoCommonService siteinfo_common.SiteInfoCommonService } // NewAuthUserMiddleware new auth user middleware func NewAuthUserMiddleware( authService *auth.AuthService, - siteInfoCommonService *siteinfo_common.SiteInfoCommonService) *AuthUserMiddleware { + siteInfoCommonService siteinfo_common.SiteInfoCommonService) *AuthUserMiddleware { return &AuthUserMiddleware{ authService: authService, siteInfoCommonService: siteInfoCommonService, @@ -140,6 +143,34 @@ func (am *AuthUserMiddleware) AdminAuth() gin.HandlerFunc { } } +func (am *AuthUserMiddleware) CheckPrivateMode() gin.HandlerFunc { + return func(ctx *gin.Context) { + resp, err := am.siteInfoCommonService.GetSiteLogin(ctx) + if err != nil { + ShowIndexPage(ctx) + ctx.Abort() + return + } + if resp.LoginRequired { + ShowIndexPage(ctx) + ctx.Abort() + return + } + ctx.Next() + } +} +func ShowIndexPage(ctx *gin.Context) { + ctx.Header("content-type", "text/html;charset=utf-8") + ctx.Header("X-Frame-Options", "DENY") + file, err := ui.Build.ReadFile("build/index.html") + if err != nil { + log.Error(err) + ctx.Status(http.StatusNotFound) + return + } + ctx.String(http.StatusOK, string(file)) +} + // GetLoginUserIDFromContext get user id from context func GetLoginUserIDFromContext(ctx *gin.Context) (userID string) { userInfo := GetUserInfoFromContext(ctx) @@ -171,6 +202,21 @@ func GetUserInfoFromContext(ctx *gin.Context) (u *entity.UserCacheInfo) { return u } +func GetUserIsAdminModerator(ctx *gin.Context) (isAdminModerator bool) { + userInfo, exist := ctx.Get(ctxUUIDKey) + if !exist { + return false + } + u, ok := userInfo.(*entity.UserCacheInfo) + if !ok { + return false + } + if u.RoleID == role.RoleAdminID || u.RoleID == role.RoleModeratorID { + return true + } + return false +} + func GetLoginUserIDInt64FromContext(ctx *gin.Context) (userID int64) { userIDStr := GetLoginUserIDFromContext(ctx) return converter.StringToInt64(userIDStr) diff --git a/internal/base/middleware/avatar.go b/internal/base/middleware/avatar.go index 82e6ceab..dadb93c3 100644 --- a/internal/base/middleware/avatar.go +++ b/internal/base/middleware/avatar.go @@ -32,31 +32,28 @@ func NewAvatarMiddleware(serviceConfig *service_config.ServiceConfig, func (am *AvatarMiddleware) AvatarThumb() gin.HandlerFunc { return func(ctx *gin.Context) { - u := ctx.Request.RequestURI - if strings.Contains(u, "/uploads/avatar/") { - sizeStr := ctx.Query("s") - size := converter.StringToInt(sizeStr) - uUrl, err := url.Parse(u) + uri := ctx.Request.RequestURI + if strings.Contains(uri, "/uploads/avatar/") { + size := converter.StringToInt(ctx.Query("s")) + uriWithoutQuery, _ := url.Parse(uri) + filename := filepath.Base(uriWithoutQuery.Path) + filePath := fmt.Sprintf("%s/avatar/%s", am.serviceConfig.UploadPath, filename) + var err error + if size != 0 { + filePath, err = am.uploaderService.AvatarThumbFile(ctx, filename, size) + if err != nil { + log.Error(err) + ctx.Abort() + } + } + avatarFile, err := os.ReadFile(filePath) if err != nil { - ctx.Next() + log.Error(err) + ctx.Abort() return } - _, urlfileName := filepath.Split(uUrl.Path) - uploadPath := am.serviceConfig.UploadPath - filePath := fmt.Sprintf("%s/avatar/%s", uploadPath, urlfileName) - var avatarfile []byte - if size == 0 { - avatarfile, err = os.ReadFile(filePath) - } else { - avatarfile, err = am.uploaderService.AvatarThumbFile(ctx, uploadPath, urlfileName, size) - } - if err != nil { - ctx.Next() - return - } - ext := strings.ToLower(path.Ext(filePath)[1:]) - ctx.Header("content-type", fmt.Sprintf("image/%s", ext)) - _, err = ctx.Writer.WriteString(string(avatarfile)) + ctx.Header("content-type", fmt.Sprintf("image/%s", strings.TrimLeft(path.Ext(filePath), "."))) + _, err = ctx.Writer.Write(avatarFile) if err != nil { log.Error(err) } @@ -64,7 +61,7 @@ func (am *AvatarMiddleware) AvatarThumb() gin.HandlerFunc { return } else { - uUrl, err := url.Parse(u) + uUrl, err := url.Parse(uri) if err != nil { ctx.Next() return diff --git a/internal/base/middleware/provider.go b/internal/base/middleware/provider.go index db89854f..7e699c91 100644 --- a/internal/base/middleware/provider.go +++ b/internal/base/middleware/provider.go @@ -8,4 +8,5 @@ import ( var ProviderSetMiddleware = wire.NewSet( NewAuthUserMiddleware, NewAvatarMiddleware, + NewShortIDMiddleware, ) diff --git a/internal/base/middleware/short_id.go b/internal/base/middleware/short_id.go new file mode 100644 index 00000000..1d1f7f6f --- /dev/null +++ b/internal/base/middleware/short_id.go @@ -0,0 +1,29 @@ +package middleware + +import ( + "github.com/answerdev/answer/internal/base/constant" + "github.com/answerdev/answer/internal/service/siteinfo_common" + "github.com/gin-gonic/gin" + "github.com/segmentfault/pacman/log" +) + +type ShortIDMiddleware struct { + siteInfoService siteinfo_common.SiteInfoCommonService +} + +func NewShortIDMiddleware(siteInfoService siteinfo_common.SiteInfoCommonService) *ShortIDMiddleware { + return &ShortIDMiddleware{ + siteInfoService: siteInfoService, + } +} + +func (sm *ShortIDMiddleware) SetShortIDFlag() gin.HandlerFunc { + return func(ctx *gin.Context) { + siteSeo, err := sm.siteInfoService.GetSiteSeo(ctx) + if err != nil { + log.Error(err) + return + } + ctx.Set(constant.ShortIDFlag, siteSeo.IsShortLink()) + } +} diff --git a/internal/base/server/http.go b/internal/base/server/http.go index 1c79dbee..faa9e046 100644 --- a/internal/base/server/http.go +++ b/internal/base/server/http.go @@ -20,6 +20,7 @@ func NewHTTPServer(debug bool, viewRouter *router.UIRouter, authUserMiddleware *middleware.AuthUserMiddleware, avatarMiddleware *middleware.AvatarMiddleware, + shortIDMiddleware *middleware.ShortIDMiddleware, templateRouter *router.TemplateRouter, pluginAPIRouter *router.PluginAPIRouter, ) *gin.Engine { @@ -30,7 +31,7 @@ func NewHTTPServer(debug bool, gin.SetMode(gin.ReleaseMode) } r := gin.New() - r.Use(brotli.Brotli(brotli.DefaultCompression), middleware.ExtractAndSetAcceptLanguage) + r.Use(brotli.Brotli(brotli.DefaultCompression), middleware.ExtractAndSetAcceptLanguage, shortIDMiddleware.SetShortIDFlag()) r.GET("/healthz", func(ctx *gin.Context) { ctx.String(200, "OK") }) html, _ := fs.Sub(ui.Template, "template") diff --git a/internal/cli/build.go b/internal/cli/build.go index 21e661e1..1c11d922 100644 --- a/internal/cli/build.go +++ b/internal/cli/build.go @@ -207,7 +207,8 @@ func copyUIFiles(b *buildingMaterial) (err error) { goModUIDir := filepath.Join(strings.TrimSpace(buf.String()), "ui") localUIBuildDir := filepath.Join(b.tmpDir, "vendor/github.com/answerdev/answer/ui/") - if err = copyDirEntries(os.DirFS(goModUIDir), ".", localUIBuildDir); err != nil { + // The node_modules folder generated during development will interfere packaging, so it needs to be ignored. + if err = copyDirEntries(os.DirFS(goModUIDir), ".", localUIBuildDir, "node_modules"); err != nil { return fmt.Errorf("failed to copy ui files: %w", err) } return nil @@ -366,15 +367,27 @@ func mergeI18nFiles(b *buildingMaterial) (err error) { return err } -func copyDirEntries(sourceFs fs.FS, sourceDir string, targetDir string) (err error) { +func copyDirEntries(sourceFs fs.FS, sourceDir, targetDir string, ignoreDir ...string) (err error) { err = dir.CreateDirIfNotExist(targetDir) if err != nil { return err } + ignoreThisDir := func(path string) bool { + for _, s := range ignoreDir { + if strings.HasPrefix(path, s) { + return true + } + } + return false + } + err = fs.WalkDir(sourceFs, sourceDir, func(path string, d fs.DirEntry, err error) error { if err != nil { return err } + if ignoreThisDir(path) { + return nil + } // Convert the path to use forward slashes, important because we use embedded FS which always uses forward slashes path = filepath.ToSlash(path) diff --git a/internal/controller/activity_controller.go b/internal/controller/activity_controller.go index bb7260cc..df1d0b02 100644 --- a/internal/controller/activity_controller.go +++ b/internal/controller/activity_controller.go @@ -5,22 +5,19 @@ import ( "github.com/answerdev/answer/internal/base/middleware" "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/role" "github.com/answerdev/answer/pkg/uid" "github.com/gin-gonic/gin" ) type ActivityController struct { - activityCommonService *activity_common.ActivityCommon - activityService *activity.ActivityService + activityService *activity.ActivityService } // NewActivityController new activity controller. func NewActivityController( - activityCommonService *activity_common.ActivityCommon, activityService *activity.ActivityService) *ActivityController { - return &ActivityController{activityCommonService: activityCommonService, activityService: activityService} + return &ActivityController{activityService: activityService} } // GetObjectTimeline get object timeline diff --git a/internal/controller/answer_controller.go b/internal/controller/answer_controller.go index 93d34f5b..81344b85 100644 --- a/internal/controller/answer_controller.go +++ b/internal/controller/answer_controller.go @@ -6,9 +6,12 @@ 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/translator" + "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/dashboard" + "github.com/answerdev/answer/internal/service/action" "github.com/answerdev/answer/internal/service/permission" "github.com/answerdev/answer/internal/service/rank" "github.com/answerdev/answer/pkg/uid" @@ -18,20 +21,21 @@ import ( // AnswerController answer controller type AnswerController struct { - answerService *service.AnswerService - rankService *rank.RankService - dashboardService *dashboard.DashboardService + answerService *service.AnswerService + rankService *rank.RankService + actionService *action.CaptchaService } // NewAnswerController new controller -func NewAnswerController(answerService *service.AnswerService, +func NewAnswerController( + answerService *service.AnswerService, rankService *rank.RankService, - dashboardService *dashboard.DashboardService, + actionService *action.CaptchaService, ) *AnswerController { return &AnswerController{ - answerService: answerService, - rankService: rankService, - dashboardService: dashboardService, + answerService: answerService, + rankService: rankService, + actionService: actionService, } } @@ -52,6 +56,19 @@ func (ac *AnswerController) RemoveAnswer(ctx *gin.Context) { } req.ID = uid.DeShortID(req.ID) req.UserID = middleware.GetLoginUserIDFromContext(ctx) + isAdmin := middleware.GetUserIsAdminModerator(ctx) + if !isAdmin { + captchaPass := ac.actionService.ActionRecordVerifyCaptcha(ctx, entity.CaptchaActionDelete, req.UserID, req.CaptchaID, req.CaptchaCode) + if !captchaPass { + errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{ + ErrorField: "captcha_code", + ErrorMsg: translator.Tr(handler.GetLang(ctx), reason.CaptchaVerificationFailed), + }) + handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields) + return + } + } + objectOwner := ac.rankService.CheckOperationObjectOwner(ctx, req.UserID, req.ID) canList, err := ac.rankService.CheckOperationPermissions(ctx, req.UserID, []string{ permission.AnswerDelete, @@ -67,6 +84,9 @@ func (ac *AnswerController) RemoveAnswer(ctx *gin.Context) { } err = ac.answerService.RemoveAnswer(ctx, req) + if !isAdmin { + ac.actionService.ActionRecordAdd(ctx, entity.CaptchaActionDelete, req.UserID) + } handler.HandleResponse(ctx, err, nil) } @@ -117,6 +137,30 @@ func (ac *AnswerController) Add(ctx *gin.Context) { req.QuestionID = uid.DeShortID(req.QuestionID) req.UserID = middleware.GetLoginUserIDFromContext(ctx) + canList, err := ac.rankService.CheckOperationPermissions(ctx, req.UserID, []string{ + permission.AnswerEdit, + permission.AnswerDelete, + permission.LinkUrlLimit, + }) + if err != nil { + handler.HandleResponse(ctx, err, nil) + return + } + + linkUrlLimitUser := canList[2] + isAdmin := middleware.GetUserIsAdminModerator(ctx) + if !isAdmin || !linkUrlLimitUser { + captchaPass := ac.actionService.ActionRecordVerifyCaptcha(ctx, entity.CaptchaActionAnswer, req.UserID, req.CaptchaID, req.CaptchaCode) + if !captchaPass { + errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{ + ErrorField: "captcha_code", + ErrorMsg: translator.Tr(handler.GetLang(ctx), reason.CaptchaVerificationFailed), + }) + handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields) + return + } + } + can, err := ac.rankService.CheckOperationPermission(ctx, req.UserID, permission.AnswerAdd, "") if err != nil { handler.HandleResponse(ctx, err, nil) @@ -132,6 +176,9 @@ func (ac *AnswerController) Add(ctx *gin.Context) { handler.HandleResponse(ctx, err, nil) return } + if !isAdmin || !linkUrlLimitUser { + ac.actionService.ActionRecordAdd(ctx, entity.CaptchaActionAnswer, req.UserID) + } info, questionInfo, has, err := ac.answerService.Get(ctx, answerID, req.UserID) if err != nil { handler.HandleResponse(ctx, err, nil) @@ -142,15 +189,6 @@ func (ac *AnswerController) Add(ctx *gin.Context) { return } - canList, err := ac.rankService.CheckOperationPermissions(ctx, req.UserID, []string{ - permission.AnswerEdit, - permission.AnswerDelete, - }) - if err != nil { - handler.HandleResponse(ctx, err, nil) - return - } - objectOwner := ac.rankService.CheckOperationObjectOwner(ctx, req.UserID, info.ID) req.CanEdit = canList[0] || objectOwner req.CanDelete = canList[1] || objectOwner @@ -181,16 +219,30 @@ func (ac *AnswerController) Update(ctx *gin.Context) { return } req.UserID = middleware.GetLoginUserIDFromContext(ctx) - req.QuestionID = uid.DeShortID(req.QuestionID) canList, err := ac.rankService.CheckOperationPermissions(ctx, req.UserID, []string{ permission.AnswerEdit, permission.AnswerEditWithoutReview, + permission.LinkUrlLimit, }) if err != nil { handler.HandleResponse(ctx, err, nil) return } + req.QuestionID = uid.DeShortID(req.QuestionID) + linkUrlLimitUser := canList[2] + isAdmin := middleware.GetUserIsAdminModerator(ctx) + if !isAdmin || !linkUrlLimitUser { + captchaPass := ac.actionService.ActionRecordVerifyCaptcha(ctx, entity.CaptchaActionEdit, req.UserID, req.CaptchaID, req.CaptchaCode) + if !captchaPass { + errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{ + ErrorField: "captcha_code", + ErrorMsg: translator.Tr(handler.GetLang(ctx), reason.CaptchaVerificationFailed), + }) + handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields) + return + } + } objectOwner := ac.rankService.CheckOperationObjectOwner(ctx, req.UserID, req.ID) req.CanEdit = canList[0] || objectOwner @@ -205,6 +257,9 @@ func (ac *AnswerController) Update(ctx *gin.Context) { handler.HandleResponse(ctx, err, nil) return } + if !isAdmin || !linkUrlLimitUser { + ac.actionService.ActionRecordAdd(ctx, entity.CaptchaActionEdit, req.UserID) + } _, _, _, err = ac.answerService.Get(ctx, req.ID, req.UserID) if err != nil { handler.HandleResponse(ctx, err, nil) diff --git a/internal/controller/comment_controller.go b/internal/controller/comment_controller.go index e998bb0d..17e65d49 100644 --- a/internal/controller/comment_controller.go +++ b/internal/controller/comment_controller.go @@ -4,7 +4,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/translator" + "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/action" "github.com/answerdev/answer/internal/service/comment" "github.com/answerdev/answer/internal/service/permission" "github.com/answerdev/answer/internal/service/rank" @@ -17,13 +21,20 @@ import ( type CommentController struct { commentService *comment.CommentService rankService *rank.RankService + actionService *action.CaptchaService } // NewCommentController new controller func NewCommentController( commentService *comment.CommentService, - rankService *rank.RankService) *CommentController { - return &CommentController{commentService: commentService, rankService: rankService} + rankService *rank.RankService, + actionService *action.CaptchaService, +) *CommentController { + return &CommentController{ + commentService: commentService, + rankService: rankService, + actionService: actionService, + } } // AddComment add comment @@ -43,15 +54,31 @@ func (cc *CommentController) AddComment(ctx *gin.Context) { } req.ObjectID = uid.DeShortID(req.ObjectID) req.UserID = middleware.GetLoginUserIDFromContext(ctx) + canList, err := cc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{ permission.CommentAdd, permission.CommentEdit, permission.CommentDelete, + permission.LinkUrlLimit, }) if err != nil { handler.HandleResponse(ctx, err, nil) return } + linkUrlLimitUser := canList[3] + isAdmin := middleware.GetUserIsAdminModerator(ctx) + if !isAdmin || !linkUrlLimitUser { + captchaPass := cc.actionService.ActionRecordVerifyCaptcha(ctx, entity.CaptchaActionComment, req.UserID, req.CaptchaID, req.CaptchaCode) + if !captchaPass { + errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{ + ErrorField: "captcha_code", + ErrorMsg: translator.Tr(handler.GetLang(ctx), reason.CaptchaVerificationFailed), + }) + handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields) + return + } + } + req.CanAdd = canList[0] req.CanEdit = canList[1] req.CanDelete = canList[2] @@ -61,6 +88,9 @@ func (cc *CommentController) AddComment(ctx *gin.Context) { } resp, err := cc.commentService.AddComment(ctx, req) + if !isAdmin || !linkUrlLimitUser { + cc.actionService.ActionRecordAdd(ctx, entity.CaptchaActionComment, req.UserID) + } handler.HandleResponse(ctx, err, resp) } @@ -81,6 +111,18 @@ func (cc *CommentController) RemoveComment(ctx *gin.Context) { } req.UserID = middleware.GetLoginUserIDFromContext(ctx) + isAdmin := middleware.GetUserIsAdminModerator(ctx) + if !isAdmin { + captchaPass := cc.actionService.ActionRecordVerifyCaptcha(ctx, entity.CaptchaActionDelete, req.UserID, req.CaptchaID, req.CaptchaCode) + if !captchaPass { + errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{ + ErrorField: "captcha_code", + ErrorMsg: translator.Tr(handler.GetLang(ctx), reason.CaptchaVerificationFailed), + }) + handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields) + return + } + } can, err := cc.rankService.CheckOperationPermission(ctx, req.UserID, permission.CommentDelete, req.CommentID) if err != nil { handler.HandleResponse(ctx, err, nil) @@ -92,6 +134,9 @@ func (cc *CommentController) RemoveComment(ctx *gin.Context) { } err = cc.commentService.RemoveComment(ctx, req) + if !isAdmin { + cc.actionService.ActionRecordAdd(ctx, entity.CaptchaActionDelete, req.UserID) + } handler.HandleResponse(ctx, err, nil) } @@ -112,16 +157,31 @@ func (cc *CommentController) UpdateComment(ctx *gin.Context) { } req.UserID = middleware.GetLoginUserIDFromContext(ctx) - req.IsAdmin = middleware.GetIsAdminFromContext(ctx) canList, err := cc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{ permission.CommentAdd, permission.CommentEdit, permission.CommentDelete, + permission.LinkUrlLimit, }) if err != nil { handler.HandleResponse(ctx, err, nil) return } + linkUrlLimitUser := canList[3] + req.IsAdmin = middleware.GetIsAdminFromContext(ctx) + isAdmin := middleware.GetUserIsAdminModerator(ctx) + if !isAdmin || !linkUrlLimitUser { + captchaPass := cc.actionService.ActionRecordVerifyCaptcha(ctx, entity.CaptchaActionEdit, req.UserID, req.CaptchaID, req.CaptchaCode) + if !captchaPass { + errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{ + ErrorField: "captcha_code", + ErrorMsg: translator.Tr(handler.GetLang(ctx), reason.CaptchaVerificationFailed), + }) + handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields) + return + } + } + req.CanAdd = canList[0] req.CanEdit = canList[1] req.CanDelete = canList[2] @@ -136,6 +196,9 @@ func (cc *CommentController) UpdateComment(ctx *gin.Context) { } resp, err := cc.commentService.UpdateComment(ctx, req) + if !isAdmin || !linkUrlLimitUser { + cc.actionService.ActionRecordAdd(ctx, entity.CaptchaActionEdit, req.UserID) + } handler.HandleResponse(ctx, err, resp) } diff --git a/internal/controller/connector_controller.go b/internal/controller/connector_controller.go index f3bfdc5a..b624e947 100644 --- a/internal/controller/connector_controller.go +++ b/internal/controller/connector_controller.go @@ -23,14 +23,14 @@ const ( // ConnectorController comment controller type ConnectorController struct { - siteInfoService *siteinfo_common.SiteInfoCommonService + siteInfoService siteinfo_common.SiteInfoCommonService userExternalService *user_external_login.UserExternalLoginService emailService *export.EmailService } // NewConnectorController new controller func NewConnectorController( - siteInfoService *siteinfo_common.SiteInfoCommonService, + siteInfoService siteinfo_common.SiteInfoCommonService, emailService *export.EmailService, userExternalService *user_external_login.UserExternalLoginService, ) *ConnectorController { @@ -93,7 +93,6 @@ func (cc *ConnectorController) ConnectorLogin(connector plugin.Connector) (fn fu if len(redirectURL) > 0 { ctx.Redirect(http.StatusFound, redirectURL) } - return } } diff --git a/internal/controller/controller.go b/internal/controller/controller.go index 6b1c2fad..db5f98c7 100644 --- a/internal/controller/controller.go +++ b/internal/controller/controller.go @@ -19,7 +19,7 @@ var ProviderSetController = wire.NewSet( NewRankController, NewReasonController, NewNotificationController, - NewSiteinfoController, + NewSiteInfoController, NewDashboardController, NewUploadController, NewActivityController, diff --git a/internal/controller/dashboard_controller.go b/internal/controller/dashboard_controller.go index a525adeb..fe7c0b26 100644 --- a/internal/controller/dashboard_controller.go +++ b/internal/controller/dashboard_controller.go @@ -7,12 +7,12 @@ import ( ) type DashboardController struct { - dashboardService *dashboard.DashboardService + dashboardService dashboard.DashboardService } // NewDashboardController new controller func NewDashboardController( - dashboardService *dashboard.DashboardService, + dashboardService dashboard.DashboardService, ) *DashboardController { return &DashboardController{ dashboardService: dashboardService, @@ -29,7 +29,7 @@ func NewDashboardController( // @Router /answer/admin/api/dashboard [get] // @Success 200 {object} handler.RespBody func (ac *DashboardController) DashboardInfo(ctx *gin.Context) { - info, err := ac.dashboardService.StatisticalByCache(ctx) + info, err := ac.dashboardService.Statistical(ctx) handler.HandleResponse(ctx, err, gin.H{ "info": info, }) diff --git a/internal/controller/lang_controller.go b/internal/controller/lang_controller.go index 0cf2637a..f2717fa8 100644 --- a/internal/controller/lang_controller.go +++ b/internal/controller/lang_controller.go @@ -12,11 +12,11 @@ import ( type LangController struct { translator i18n.Translator - siteInfoService *siteinfo_common.SiteInfoCommonService + siteInfoService siteinfo_common.SiteInfoCommonService } // NewLangController new language controller. -func NewLangController(tr i18n.Translator, siteInfoService *siteinfo_common.SiteInfoCommonService) *LangController { +func NewLangController(tr i18n.Translator, siteInfoService siteinfo_common.SiteInfoCommonService) *LangController { return &LangController{translator: tr, siteInfoService: siteInfoService} } diff --git a/internal/controller/plugin_user_center_controller.go b/internal/controller/plugin_user_center_controller.go index 024a0692..c4476065 100644 --- a/internal/controller/plugin_user_center_controller.go +++ b/internal/controller/plugin_user_center_controller.go @@ -22,13 +22,13 @@ const ( // UserCenterController comment controller type UserCenterController struct { userCenterLoginService *user_external_login.UserCenterLoginService - siteInfoService *siteinfo_common.SiteInfoCommonService + siteInfoService siteinfo_common.SiteInfoCommonService } // NewUserCenterController new controller func NewUserCenterController( userCenterLoginService *user_external_login.UserCenterLoginService, - siteInfoService *siteinfo_common.SiteInfoCommonService, + siteInfoService siteinfo_common.SiteInfoCommonService, ) *UserCenterController { return &UserCenterController{ userCenterLoginService: userCenterLoginService, diff --git a/internal/controller/question_controller.go b/internal/controller/question_controller.go index a0c35be6..71fc24ec 100644 --- a/internal/controller/question_controller.go +++ b/internal/controller/question_controller.go @@ -10,8 +10,10 @@ import ( "github.com/answerdev/answer/internal/entity" "github.com/answerdev/answer/internal/schema" "github.com/answerdev/answer/internal/service" + "github.com/answerdev/answer/internal/service/action" "github.com/answerdev/answer/internal/service/permission" "github.com/answerdev/answer/internal/service/rank" + "github.com/answerdev/answer/internal/service/siteinfo_common" "github.com/answerdev/answer/pkg/uid" "github.com/gin-gonic/gin" "github.com/jinzhu/copier" @@ -23,6 +25,8 @@ type QuestionController struct { questionService *service.QuestionService answerService *service.AnswerService rankService *rank.RankService + siteInfoService siteinfo_common.SiteInfoCommonService + actionService *action.CaptchaService } // NewQuestionController new controller @@ -30,11 +34,15 @@ func NewQuestionController( questionService *service.QuestionService, answerService *service.AnswerService, rankService *rank.RankService, + siteInfoService siteinfo_common.SiteInfoCommonService, + actionService *action.CaptchaService, ) *QuestionController { return &QuestionController{ questionService: questionService, answerService: answerService, rankService: rankService, + siteInfoService: siteInfoService, + actionService: actionService, } } @@ -56,6 +64,19 @@ func (qc *QuestionController) RemoveQuestion(ctx *gin.Context) { req.ID = uid.DeShortID(req.ID) req.UserID = middleware.GetLoginUserIDFromContext(ctx) req.IsAdmin = middleware.GetIsAdminFromContext(ctx) + isAdmin := middleware.GetUserIsAdminModerator(ctx) + if !isAdmin { + captchaPass := qc.actionService.ActionRecordVerifyCaptcha(ctx, entity.CaptchaActionDelete, req.UserID, req.CaptchaID, req.CaptchaCode) + if !captchaPass { + errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{ + ErrorField: "captcha_code", + ErrorMsg: translator.Tr(handler.GetLang(ctx), reason.CaptchaVerificationFailed), + }) + handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields) + return + } + } + can, err := qc.rankService.CheckOperationPermission(ctx, req.UserID, permission.QuestionDelete, req.ID) if err != nil { handler.HandleResponse(ctx, err, nil) @@ -65,8 +86,10 @@ func (qc *QuestionController) RemoveQuestion(ctx *gin.Context) { handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil) return } - err = qc.questionService.RemoveQuestion(ctx, req) + if !isAdmin { + qc.actionService.ActionRecordAdd(ctx, entity.CaptchaActionDelete, req.UserID) + } handler.HandleResponse(ctx, err, nil) } @@ -220,7 +243,9 @@ func (qc *QuestionController) GetQuestion(ctx *gin.Context) { handler.HandleResponse(ctx, err, nil) return } - info.ID = uid.EnShortID(info.ID) + if handler.GetEnableShortID(ctx) { + info.ID = uid.EnShortID(info.ID) + } handler.HandleResponse(ctx, nil, info) } @@ -305,8 +330,8 @@ func (qc *QuestionController) AddQuestion(ctx *gin.Context) { if ctx.IsAborted() { return } - req.UserID = middleware.GetLoginUserIDFromContext(ctx) + req.UserID = middleware.GetLoginUserIDFromContext(ctx) canList, requireRanks, err := qc.rankService.CheckOperationPermissionsForRanks(ctx, req.UserID, []string{ permission.QuestionAdd, permission.QuestionEdit, @@ -315,11 +340,26 @@ func (qc *QuestionController) AddQuestion(ctx *gin.Context) { permission.QuestionReopen, permission.TagUseReservedTag, permission.TagAdd, + permission.LinkUrlLimit, }) if err != nil { handler.HandleResponse(ctx, err, nil) return } + linkUrlLimitUser := canList[7] + isAdmin := middleware.GetUserIsAdminModerator(ctx) + if !isAdmin || !linkUrlLimitUser { + captchaPass := qc.actionService.ActionRecordVerifyCaptcha(ctx, entity.CaptchaActionQuestion, req.UserID, req.CaptchaID, req.CaptchaCode) + if !captchaPass { + errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{ + ErrorField: "captcha_code", + ErrorMsg: translator.Tr(handler.GetLang(ctx), reason.CaptchaVerificationFailed), + }) + handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields) + return + } + } + req.CanAdd = canList[0] req.CanEdit = canList[1] req.CanDelete = canList[2] @@ -370,7 +410,9 @@ func (qc *QuestionController) AddQuestion(ctx *gin.Context) { handler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), errFields) return } - + if !isAdmin || !linkUrlLimitUser { + qc.actionService.ActionRecordAdd(ctx, entity.CaptchaActionQuestion, req.UserID) + } handler.HandleResponse(ctx, err, resp) } @@ -399,11 +441,26 @@ func (qc *QuestionController) AddQuestionByAnswer(ctx *gin.Context) { permission.QuestionClose, permission.QuestionReopen, permission.TagUseReservedTag, + permission.LinkUrlLimit, }) if err != nil { handler.HandleResponse(ctx, err, nil) return } + + linkUrlLimitUser := canList[6] + isAdmin := middleware.GetUserIsAdminModerator(ctx) + if !isAdmin || !linkUrlLimitUser { + captchaPass := qc.actionService.ActionRecordVerifyCaptcha(ctx, entity.CaptchaActionQuestion, req.UserID, req.CaptchaID, req.CaptchaCode) + if !captchaPass { + errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{ + ErrorField: "captcha_code", + ErrorMsg: translator.Tr(handler.GetLang(ctx), reason.CaptchaVerificationFailed), + }) + handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields) + return + } + } req.CanAdd = canList[0] req.CanEdit = canList[1] req.CanDelete = canList[2] @@ -441,6 +498,10 @@ func (qc *QuestionController) AddQuestionByAnswer(ctx *gin.Context) { } } + if !isAdmin || !linkUrlLimitUser { + qc.actionService.ActionRecordAdd(ctx, entity.CaptchaActionQuestion, req.UserID) + } + if len(errFields) > 0 { handler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), errFields) return @@ -495,18 +556,31 @@ func (qc *QuestionController) UpdateQuestion(ctx *gin.Context) { } req.ID = uid.DeShortID(req.ID) req.UserID = middleware.GetLoginUserIDFromContext(ctx) - canList, requireRanks, err := qc.rankService.CheckOperationPermissionsForRanks(ctx, req.UserID, []string{ permission.QuestionEdit, permission.QuestionDelete, permission.QuestionEditWithoutReview, permission.TagUseReservedTag, permission.TagAdd, + permission.LinkUrlLimit, }) if err != nil { handler.HandleResponse(ctx, err, nil) return } + linkUrlLimitUser := canList[5] + isAdmin := middleware.GetUserIsAdminModerator(ctx) + if !isAdmin || !linkUrlLimitUser { + captchaPass := qc.actionService.ActionRecordVerifyCaptcha(ctx, entity.CaptchaActionEdit, req.UserID, req.CaptchaID, req.CaptchaCode) + if !captchaPass { + errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{ + ErrorField: "captcha_code", + ErrorMsg: translator.Tr(handler.GetLang(ctx), reason.CaptchaVerificationFailed), + }) + handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields) + return + } + } objectOwner := qc.rankService.CheckOperationObjectOwner(ctx, req.UserID, req.ID) req.CanEdit = canList[0] || objectOwner @@ -547,6 +621,9 @@ func (qc *QuestionController) UpdateQuestion(ctx *gin.Context) { handler.HandleResponse(ctx, err, resp) return } + if !isAdmin || !linkUrlLimitUser { + qc.actionService.ActionRecordAdd(ctx, entity.CaptchaActionEdit, req.UserID) + } handler.HandleResponse(ctx, nil, &schema.UpdateQuestionResp{WaitForReview: !req.NoNeedReview}) } @@ -566,8 +643,24 @@ func (qc *QuestionController) UpdateQuestionInviteUser(ctx *gin.Context) { if ctx.IsAborted() { return } + if len(errFields) > 0 { + handler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), errFields) + return + } req.ID = uid.DeShortID(req.ID) req.UserID = middleware.GetLoginUserIDFromContext(ctx) + isAdmin := middleware.GetUserIsAdminModerator(ctx) + if !isAdmin { + captchaPass := qc.actionService.ActionRecordVerifyCaptcha(ctx, entity.CaptchaActionInvitationAnswer, req.UserID, req.CaptchaID, req.CaptchaCode) + if !captchaPass { + errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{ + ErrorField: "captcha_code", + ErrorMsg: translator.Tr(handler.GetLang(ctx), reason.CaptchaVerificationFailed), + }) + handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields) + return + } + } canList, err := qc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{ permission.AnswerInviteSomeoneToAnswer, @@ -577,21 +670,19 @@ func (qc *QuestionController) UpdateQuestionInviteUser(ctx *gin.Context) { return } - objectOwner := qc.rankService.CheckOperationObjectOwner(ctx, req.UserID, req.ID) - req.CanEdit = canList[0] || objectOwner - if !req.CanEdit { + req.CanInviteOtherToAnswer = canList[0] + if !req.CanInviteOtherToAnswer { handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil) return } - if len(errFields) > 0 { - handler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), errFields) - return - } err = qc.questionService.UpdateQuestionInviteUser(ctx, req) if err != nil { handler.HandleResponse(ctx, err, nil) return } + if !isAdmin { + qc.actionService.ActionRecordAdd(ctx, entity.CaptchaActionInvitationAnswer, req.UserID) + } handler.HandleResponse(ctx, nil, nil) } @@ -703,8 +794,8 @@ func (qc *QuestionController) PersonalCollectionPage(ctx *gin.Context) { handler.HandleResponse(ctx, err, resp) } -// AdminSearchList godoc -// @Summary AdminSearchList +// AdminQuestionPage admin question page +// @Summary AdminQuestionPage admin question page // @Description Status:[available,closed,deleted] // @Tags admin // @Accept json @@ -716,21 +807,19 @@ func (qc *QuestionController) PersonalCollectionPage(ctx *gin.Context) { // @Param query query string false "question id or title" // @Success 200 {object} handler.RespBody // @Router /answer/admin/api/question/page [get] -func (qc *QuestionController) AdminSearchList(ctx *gin.Context) { - req := &schema.AdminQuestionSearch{} +func (qc *QuestionController) AdminQuestionPage(ctx *gin.Context) { + req := &schema.AdminQuestionPageReq{} if handler.BindAndCheck(ctx, req) { return } - userID := middleware.GetLoginUserIDFromContext(ctx) - questionList, count, err := qc.questionService.AdminSearchList(ctx, req, userID) - handler.HandleResponse(ctx, err, gin.H{ - "list": questionList, - "count": count, - }) + + req.LoginUserID = middleware.GetLoginUserIDFromContext(ctx) + resp, err := qc.questionService.AdminQuestionPage(ctx, req) + handler.HandleResponse(ctx, err, resp) } -// AdminSearchAnswerList godoc -// @Summary AdminSearchAnswerList +// AdminAnswerPage admin answer page +// @Summary AdminAnswerPage admin answer page // @Description Status:[available,deleted] // @Tags admin // @Accept json @@ -743,21 +832,15 @@ func (qc *QuestionController) AdminSearchList(ctx *gin.Context) { // @Param question_id query string false "question id" // @Success 200 {object} handler.RespBody // @Router /answer/admin/api/answer/page [get] -func (qc *QuestionController) AdminSearchAnswerList(ctx *gin.Context) { - req := &entity.AdminAnswerSearch{} +func (qc *QuestionController) AdminAnswerPage(ctx *gin.Context) { + req := &schema.AdminAnswerPageReq{} if handler.BindAndCheck(ctx, req) { return } - req.QuestionID = uid.DeShortID(req.QuestionID) - if req.QuestionID == "0" { - req.QuestionID = "" - } - userID := middleware.GetLoginUserIDFromContext(ctx) - questionList, count, err := qc.questionService.AdminSearchAnswerList(ctx, req, userID) - handler.HandleResponse(ctx, err, gin.H{ - "list": questionList, - "count": count, - }) + + req.LoginUserID = middleware.GetLoginUserIDFromContext(ctx) + resp, err := qc.questionService.AdminAnswerPage(ctx, req) + handler.HandleResponse(ctx, err, resp) } // AdminSetQuestionStatus godoc diff --git a/internal/controller/report_controller.go b/internal/controller/report_controller.go index a8196c13..6b8a3ab2 100644 --- a/internal/controller/report_controller.go +++ b/internal/controller/report_controller.go @@ -4,7 +4,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/translator" + "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/action" "github.com/answerdev/answer/internal/service/permission" "github.com/answerdev/answer/internal/service/rank" "github.com/answerdev/answer/internal/service/report" @@ -17,11 +21,20 @@ import ( type ReportController struct { reportService *report.ReportService rankService *rank.RankService + actionService *action.CaptchaService } // NewReportController new controller -func NewReportController(reportService *report.ReportService, rankService *rank.RankService) *ReportController { - return &ReportController{reportService: reportService, rankService: rankService} +func NewReportController( + reportService *report.ReportService, + rankService *rank.RankService, + actionService *action.CaptchaService, +) *ReportController { + return &ReportController{ + reportService: reportService, + rankService: rankService, + actionService: actionService, + } } // AddReport add report @@ -42,6 +55,19 @@ func (rc *ReportController) AddReport(ctx *gin.Context) { } req.ObjectID = uid.DeShortID(req.ObjectID) req.UserID = middleware.GetLoginUserIDFromContext(ctx) + isAdmin := middleware.GetUserIsAdminModerator(ctx) + if !isAdmin { + captchaPass := rc.actionService.ActionRecordVerifyCaptcha(ctx, entity.CaptchaActionReport, req.UserID, req.CaptchaID, req.CaptchaCode) + if !captchaPass { + errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{ + ErrorField: "captcha_code", + ErrorMsg: translator.Tr(handler.GetLang(ctx), reason.CaptchaVerificationFailed), + }) + handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields) + return + } + } + can, err := rc.rankService.CheckOperationPermission(ctx, req.UserID, permission.ReportAdd, "") if err != nil { handler.HandleResponse(ctx, err, nil) @@ -53,5 +79,8 @@ func (rc *ReportController) AddReport(ctx *gin.Context) { } err = rc.reportService.AddReport(ctx, req) + if !isAdmin { + rc.actionService.ActionRecordAdd(ctx, entity.CaptchaActionReport, req.UserID) + } handler.HandleResponse(ctx, err, nil) } diff --git a/internal/controller/search_controller.go b/internal/controller/search_controller.go index 5b658351..1d135451 100644 --- a/internal/controller/search_controller.go +++ b/internal/controller/search_controller.go @@ -3,19 +3,32 @@ package controller 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/translator" + "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/action" "github.com/gin-gonic/gin" + "github.com/segmentfault/pacman/errors" ) // SearchController tag controller type SearchController struct { searchService *service.SearchService + actionService *action.CaptchaService } // NewSearchController new controller -func NewSearchController(searchService *service.SearchService) *SearchController { - return &SearchController{searchService: searchService} +func NewSearchController( + searchService *service.SearchService, + actionService *action.CaptchaService, +) *SearchController { + return &SearchController{ + searchService: searchService, + actionService: actionService, + } } // Search godoc @@ -35,12 +48,29 @@ func (sc *SearchController) Search(ctx *gin.Context) { return } dto.UserID = middleware.GetLoginUserIDFromContext(ctx) + unit := ctx.ClientIP() + if dto.UserID != "" { + unit = dto.UserID + } + isAdmin := middleware.GetUserIsAdminModerator(ctx) + if !isAdmin { + captchaPass := sc.actionService.ActionRecordVerifyCaptcha(ctx, entity.CaptchaActionSearch, unit, dto.CaptchaID, dto.CaptchaCode) + if !captchaPass { + errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{ + ErrorField: "captcha_code", + ErrorMsg: translator.Tr(handler.GetLang(ctx), reason.CaptchaVerificationFailed), + }) + handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields) + return + } + } - resp, total, extra, err := sc.searchService.Search(ctx, &dto) - + resp, total, err := sc.searchService.Search(ctx, &dto) + if !isAdmin { + sc.actionService.ActionRecordAdd(ctx, entity.CaptchaActionSearch, unit) + } handler.HandleResponse(ctx, err, schema.SearchListResp{ Total: total, SearchResp: resp, - Extra: extra, }) } diff --git a/internal/controller/siteinfo_controller.go b/internal/controller/siteinfo_controller.go index f815bdb9..4cdba56f 100644 --- a/internal/controller/siteinfo_controller.go +++ b/internal/controller/siteinfo_controller.go @@ -11,13 +11,13 @@ import ( "github.com/segmentfault/pacman/log" ) -type SiteinfoController struct { - siteInfoService *siteinfo_common.SiteInfoCommonService +type SiteInfoController struct { + siteInfoService siteinfo_common.SiteInfoCommonService } -// NewSiteinfoController new siteinfo controller. -func NewSiteinfoController(siteInfoService *siteinfo_common.SiteInfoCommonService) *SiteinfoController { - return &SiteinfoController{ +// NewSiteInfoController new site info controller. +func NewSiteInfoController(siteInfoService siteinfo_common.SiteInfoCommonService) *SiteInfoController { + return &SiteInfoController{ siteInfoService: siteInfoService, } } @@ -29,7 +29,7 @@ func NewSiteinfoController(siteInfoService *siteinfo_common.SiteInfoCommonServic // @Produce json // @Success 200 {object} handler.RespBody{data=schema.SiteInfoResp} // @Router /answer/api/v1/siteinfo [get] -func (sc *SiteinfoController) GetSiteInfo(ctx *gin.Context) { +func (sc *SiteInfoController) GetSiteInfo(ctx *gin.Context) { var err error resp := &schema.SiteInfoResp{Version: constant.Version, Revision: constant.Revision} resp.General, err = sc.siteInfoService.GetSiteGeneral(ctx) @@ -80,7 +80,7 @@ func (sc *SiteinfoController) GetSiteInfo(ctx *gin.Context) { // @Produce json // @Success 200 {object} handler.RespBody{data=schema.GetSiteLegalInfoResp} // @Router /answer/api/v1/siteinfo/legal [get] -func (sc *SiteinfoController) GetSiteLegalInfo(ctx *gin.Context) { +func (sc *SiteInfoController) GetSiteLegalInfo(ctx *gin.Context) { req := &schema.GetSiteLegalInfoReq{} if handler.BindAndCheck(ctx, req) { return @@ -102,7 +102,7 @@ func (sc *SiteinfoController) GetSiteLegalInfo(ctx *gin.Context) { } // GetManifestJson get manifest.json -func (sc *SiteinfoController) GetManifestJson(ctx *gin.Context) { +func (sc *SiteInfoController) GetManifestJson(ctx *gin.Context) { favicon := "favicon.ico" resp := &schema.GetManifestJsonResp{ ManifestVersion: 3, diff --git a/internal/controller/template_controller.go b/internal/controller/template_controller.go index c05c013f..bc5a753f 100644 --- a/internal/controller/template_controller.go +++ b/internal/controller/template_controller.go @@ -30,13 +30,13 @@ type TemplateController struct { scriptPath string cssPath string templateRenderController *templaterender.TemplateRenderController - siteInfoService *siteinfo_common.SiteInfoCommonService + siteInfoService siteinfo_common.SiteInfoCommonService } // NewTemplateController new controller func NewTemplateController( templateRenderController *templaterender.TemplateRenderController, - siteInfoService *siteinfo_common.SiteInfoCommonService, + siteInfoService siteinfo_common.SiteInfoCommonService, ) *TemplateController { script, css := GetStyle() return &TemplateController{ @@ -116,7 +116,7 @@ func (tc *TemplateController) Index(ctx *gin.Context) { siteInfo.Canonical = siteInfo.General.SiteUrl UrlUseTitle := false - if siteInfo.SiteSeo.PermaLink == schema.PermaLinkQuestionIDAndTitle { + if siteInfo.SiteSeo.PermaLink == constant.PermaLinkQuestionIDAndTitle { UrlUseTitle = true } siteInfo.Title = "" @@ -149,7 +149,7 @@ func (tc *TemplateController) QuestionList(ctx *gin.Context) { } UrlUseTitle := false - if siteInfo.SiteSeo.PermaLink == schema.PermaLinkQuestionIDAndTitle { + if siteInfo.SiteSeo.PermaLink == constant.PermaLinkQuestionIDAndTitle { UrlUseTitle = true } siteInfo.Title = fmt.Sprintf("Questions - %s", siteInfo.General.Name) @@ -164,16 +164,21 @@ func (tc *TemplateController) QuestionInfoeRdirect(ctx *gin.Context, siteInfo *s id := ctx.Param("id") title := ctx.Param("title") titleIsAnswerID := false - NeedChangeShortID := false + needChangeShortID := false + + siteSeo, err := tc.siteInfoService.GetSiteSeo(ctx) + if err != nil { + return false, "" + } isShortID := uid.IsShortID(id) - if uid.ShortIDSwitch { + if siteSeo.IsShortLink() { if !isShortID { id = uid.EnShortID(id) - NeedChangeShortID = true + needChangeShortID = true } } else { if isShortID { - NeedChangeShortID = true + needChangeShortID = true id = uid.DeShortID(id) } } @@ -184,13 +189,12 @@ func (tc *TemplateController) QuestionInfoeRdirect(ctx *gin.Context, siteInfo *s titleIsAnswerID = true } } - siteInfo = tc.SiteInfo(ctx) url = fmt.Sprintf("%s/questions/%s", siteInfo.General.SiteUrl, id) - if siteInfo.SiteSeo.PermaLink == schema.PermaLinkQuestionID || siteInfo.SiteSeo.PermaLink == schema.PermaLinkQuestionIDByShortID { + if siteInfo.SiteSeo.PermaLink == constant.PermaLinkQuestionID || siteInfo.SiteSeo.PermaLink == constant.PermaLinkQuestionIDByShortID { if len(ctx.Request.URL.Query()) > 0 { url = fmt.Sprintf("%s?%s", url, ctx.Request.URL.RawQuery) } - if NeedChangeShortID { + if needChangeShortID { return true, url } //not have title @@ -216,7 +220,7 @@ func (tc *TemplateController) QuestionInfoeRdirect(ctx *gin.Context, siteInfo *s } //have title if len(title) > 0 && !titleIsAnswerID && correctTitle { - if NeedChangeShortID { + if needChangeShortID { return true, url } return false, "" @@ -289,7 +293,7 @@ func (tc *TemplateController) QuestionInfo(ctx *gin.Context) { return } siteInfo.Canonical = fmt.Sprintf("%s/questions/%s/%s", siteInfo.General.SiteUrl, id, encodeTitle) - if siteInfo.SiteSeo.PermaLink == schema.PermaLinkQuestionID { + if siteInfo.SiteSeo.PermaLink == constant.PermaLinkQuestionID { siteInfo.Canonical = fmt.Sprintf("%s/questions/%s", siteInfo.General.SiteUrl, id) } jsonLD := &schema.QAPageJsonLD{} @@ -404,7 +408,7 @@ func (tc *TemplateController) TagInfo(ctx *gin.Context) { siteInfo.Keywords = taginifo.DisplayName UrlUseTitle := false - if siteInfo.SiteSeo.PermaLink == schema.PermaLinkQuestionIDAndTitle { + if siteInfo.SiteSeo.PermaLink == constant.PermaLinkQuestionIDAndTitle { UrlUseTitle = true } siteInfo.Title = fmt.Sprintf("'%s' Questions - %s", taginifo.DisplayName, siteInfo.General.Name) diff --git a/internal/controller/template_render/controller.go b/internal/controller/template_render/controller.go index f44c1ea6..51a0fdd3 100644 --- a/internal/controller/template_render/controller.go +++ b/internal/controller/template_render/controller.go @@ -1,16 +1,16 @@ package templaterender import ( + questioncommon "github.com/answerdev/answer/internal/service/question_common" "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/google/wire" "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. @@ -24,8 +24,8 @@ type TemplateRenderController struct { tagService *tag.TagService answerService *service.AnswerService commentService *comment.CommentService - data *data.Data - siteInfoService *siteinfo_common.SiteInfoCommonService + siteInfoService siteinfo_common.SiteInfoCommonService + questionRepo questioncommon.QuestionRepo } func NewTemplateRenderController( @@ -34,9 +34,8 @@ func NewTemplateRenderController( tagService *tag.TagService, answerService *service.AnswerService, commentService *comment.CommentService, - data *data.Data, - siteInfoService *siteinfo_common.SiteInfoCommonService, - + siteInfoService siteinfo_common.SiteInfoCommonService, + questionRepo questioncommon.QuestionRepo, ) *TemplateRenderController { return &TemplateRenderController{ questionService: questionService, @@ -44,7 +43,7 @@ func NewTemplateRenderController( tagService: tagService, answerService: answerService, commentService: commentService, - data: data, + questionRepo: questionRepo, siteInfoService: siteInfoService, } } diff --git a/internal/controller/template_render/index.go b/internal/controller/template_render/index.go deleted file mode 100644 index 362739ec..00000000 --- a/internal/controller/template_render/index.go +++ /dev/null @@ -1 +0,0 @@ -package templaterender diff --git a/internal/controller/template_render/question.go b/internal/controller/template_render/question.go index 6504d7e9..5629245d 100644 --- a/internal/controller/template_render/question.go +++ b/internal/controller/template_render/question.go @@ -1,11 +1,11 @@ package templaterender import ( - "encoding/json" - "fmt" "html/template" + "math" "net/http" + "github.com/answerdev/answer/internal/base/constant" "github.com/answerdev/answer/internal/schema" "github.com/gin-gonic/gin" "github.com/segmentfault/pacman/log" @@ -31,48 +31,46 @@ func (t *TemplateRenderController) Sitemap(ctx *gin.Context) { return } - sitemapInfo := &schema.SiteMapList{} - infoStr, err := t.data.Cache.GetString(ctx, schema.SitemapCachekey) + questions, err := t.questionRepo.SitemapQuestions(ctx, 1, constant.SitemapMaxSize) if err != nil { - log.Errorf("get Cache failed: %s", err) - return - } - hasTitle := false - if siteInfo.PermaLink == schema.PermaLinkQuestionIDAndTitle || siteInfo.PermaLink == schema.PermaLinkQuestionIDAndTitleByShortID { - hasTitle = true - } - if err = json.Unmarshal([]byte(infoStr), sitemapInfo); err != nil { - log.Errorf("get sitemap info failed: %s", err) + log.Errorf("get sitemap questions failed: %s", err) return } - if len(sitemapInfo.QuestionIDs) > 0 { - //question url list - ctx.Header("Content-Type", "application/xml") + ctx.Header("Content-Type", "application/xml") + if len(questions) < constant.SitemapMaxSize { ctx.HTML( http.StatusOK, "sitemap.xml", gin.H{ "xmlHeader": template.HTML(``), - "list": sitemapInfo.QuestionIDs, - "general": general, - "hastitle": hasTitle, - }, - ) - } 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, + "list": questions, "general": general, + "hastitle": siteInfo.PermaLink == constant.PermaLinkQuestionIDAndTitle || + siteInfo.PermaLink == constant.PermaLinkQuestionIDAndTitleByShortID, }, ) return } + + questionNum, err := t.questionRepo.GetQuestionCount(ctx) + if err != nil { + log.Error("GetQuestionCount error", err) + return + } + var pageList []int + totalPages := int(math.Ceil(float64(questionNum) / float64(constant.SitemapMaxSize))) + for i := 1; i <= totalPages; i++ { + pageList = append(pageList, i) + } + ctx.HTML( + http.StatusOK, "sitemap-list.xml", gin.H{ + "xmlHeader": template.HTML(``), + "page": pageList, + "general": general, + }, + ) } 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) @@ -83,28 +81,20 @@ func (t *TemplateRenderController) SitemapPage(ctx *gin.Context, page int) error log.Error("get site GetSiteSeo failed:", err) return err } - hasTitle := false - if siteInfo.PermaLink == schema.PermaLinkQuestionIDAndTitle || siteInfo.PermaLink == schema.PermaLinkQuestionIDAndTitleByShortID { - hasTitle = true - } - cachekey := fmt.Sprintf(schema.SitemapPageCachekey, page) - infoStr, err := t.data.Cache.GetString(ctx, cachekey) + questions, err := t.questionRepo.SitemapQuestions(ctx, page, constant.SitemapMaxSize) 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) + log.Errorf("get sitemap questions 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, + "list": questions, "general": general, - "hastitle": hasTitle, + "hastitle": siteInfo.PermaLink == constant.PermaLinkQuestionIDAndTitle || + siteInfo.PermaLink == constant.PermaLinkQuestionIDAndTitleByShortID, }, ) return nil diff --git a/internal/controller/user_controller.go b/internal/controller/user_controller.go index 598d7008..5f968f2c 100644 --- a/internal/controller/user_controller.go +++ b/internal/controller/user_controller.go @@ -6,13 +6,13 @@ import ( "github.com/answerdev/answer/internal/base/reason" "github.com/answerdev/answer/internal/base/translator" "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/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/answerdev/answer/pkg/checker" "github.com/gin-gonic/gin" "github.com/segmentfault/pacman/errors" @@ -24,9 +24,8 @@ type UserController struct { userService *service.UserService authService *auth.AuthService actionService *action.CaptchaService - uploaderService uploader.UploaderService emailService *export.EmailService - siteInfoCommonService *siteinfo_common.SiteInfoCommonService + siteInfoCommonService siteinfo_common.SiteInfoCommonService } // NewUserController new controller @@ -35,14 +34,12 @@ func NewUserController( userService *service.UserService, actionService *action.CaptchaService, emailService *export.EmailService, - uploaderService uploader.UploaderService, - siteInfoCommonService *siteinfo_common.SiteInfoCommonService, + siteInfoCommonService siteinfo_common.SiteInfoCommonService, ) *UserController { return &UserController{ authService: authService, userService: userService, actionService: actionService, - uploaderService: uploaderService, emailService: emailService, siteInfoCommonService: siteInfoCommonService, } @@ -109,20 +106,22 @@ func (uc *UserController) UserEmailLogin(ctx *gin.Context) { if handler.BindAndCheck(ctx, req) { return } - - captchaPass := uc.actionService.ActionRecordVerifyCaptcha(ctx, schema.ActionRecordTypeLogin, ctx.ClientIP(), req.CaptchaID, req.CaptchaCode) - if !captchaPass { - errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{ - ErrorField: "captcha_code", - ErrorMsg: translator.Tr(handler.GetLang(ctx), reason.CaptchaVerificationFailed), - }) - handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields) - return + isAdmin := middleware.GetUserIsAdminModerator(ctx) + if !isAdmin { + captchaPass := uc.actionService.ActionRecordVerifyCaptcha(ctx, entity.CaptchaActionPassword, ctx.ClientIP(), req.CaptchaID, req.CaptchaCode) + if !captchaPass { + errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{ + ErrorField: "captcha_code", + ErrorMsg: translator.Tr(handler.GetLang(ctx), reason.CaptchaVerificationFailed), + }) + handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields) + return + } } resp, err := uc.userService.EmailLogin(ctx, req) if err != nil { - _, _ = uc.actionService.ActionRecordAdd(ctx, schema.ActionRecordTypeLogin, ctx.ClientIP()) + _, _ = uc.actionService.ActionRecordAdd(ctx, entity.CaptchaActionEmail, ctx.ClientIP()) errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{ ErrorField: "e_mail", ErrorMsg: translator.Tr(handler.GetLang(ctx), reason.EmailOrPasswordWrong), @@ -130,7 +129,9 @@ func (uc *UserController) UserEmailLogin(ctx *gin.Context) { handler.HandleResponse(ctx, errors.BadRequest(reason.EmailOrPasswordWrong), errFields) return } - uc.actionService.ActionRecordDel(ctx, schema.ActionRecordTypeLogin, ctx.ClientIP()) + if !isAdmin { + uc.actionService.ActionRecordDel(ctx, entity.CaptchaActionPassword, ctx.ClientIP()) + } handler.HandleResponse(ctx, nil, resp) } @@ -148,16 +149,18 @@ func (uc *UserController) RetrievePassWord(ctx *gin.Context) { if handler.BindAndCheck(ctx, req) { return } - captchaPass := uc.actionService.ActionRecordVerifyCaptcha(ctx, schema.ActionRecordTypeFindPass, ctx.ClientIP(), req.CaptchaID, req.CaptchaCode) - if !captchaPass { - errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{ - ErrorField: "captcha_code", - ErrorMsg: translator.Tr(handler.GetLang(ctx), reason.CaptchaVerificationFailed), - }) - handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields) - return + isAdmin := middleware.GetUserIsAdminModerator(ctx) + if !isAdmin { + captchaPass := uc.actionService.ActionRecordVerifyCaptcha(ctx, entity.CaptchaActionEmail, ctx.ClientIP(), req.CaptchaID, req.CaptchaCode) + if !captchaPass { + errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{ + ErrorField: "captcha_code", + ErrorMsg: translator.Tr(handler.GetLang(ctx), reason.CaptchaVerificationFailed), + }) + handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields) + return + } } - _, _ = uc.actionService.ActionRecordAdd(ctx, schema.ActionRecordTypeFindPass, ctx.ClientIP()) err := uc.userService.RetrievePassWord(ctx, req) handler.HandleResponse(ctx, err, nil) } @@ -185,7 +188,7 @@ func (uc *UserController) UseRePassWord(ctx *gin.Context) { } err := uc.userService.UpdatePasswordWhenForgot(ctx, req) - uc.actionService.ActionRecordDel(ctx, schema.ActionRecordTypeFindPass, ctx.ClientIP()) + uc.actionService.ActionRecordDel(ctx, entity.CaptchaActionPassword, ctx.ClientIP()) handler.HandleResponse(ctx, err, nil) } @@ -238,14 +241,17 @@ func (uc *UserController) UserRegisterByEmail(ctx *gin.Context) { 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.Tr(handler.GetLang(ctx), reason.CaptchaVerificationFailed), - }) - handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields) - return + isAdmin := middleware.GetUserIsAdminModerator(ctx) + if !isAdmin { + captchaPass := uc.actionService.ActionRecordVerifyCaptcha(ctx, entity.CaptchaActionEmail, req.IP, req.CaptchaID, req.CaptchaCode) + if !captchaPass { + errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{ + ErrorField: "captcha_code", + ErrorMsg: translator.Tr(handler.GetLang(ctx), reason.CaptchaVerificationFailed), + }) + handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields) + return + } } resp, errFields, err := uc.userService.UserRegisterByEmail(ctx, req) @@ -288,7 +294,7 @@ func (uc *UserController) UserVerifyEmail(ctx *gin.Context) { return } - uc.actionService.ActionRecordDel(ctx, schema.ActionRecordTypeEmail, ctx.ClientIP()) + uc.actionService.ActionRecordDel(ctx, entity.CaptchaActionEmail, ctx.ClientIP()) handler.HandleResponse(ctx, err, resp) } @@ -313,22 +319,20 @@ func (uc *UserController) UserVerifyEmailSend(ctx *gin.Context) { handler.HandleResponse(ctx, errors.Unauthorized(reason.UnauthorizedError), nil) return } + isAdmin := middleware.GetUserIsAdminModerator(ctx) + if !isAdmin { + captchaPass := uc.actionService.ActionRecordVerifyCaptcha(ctx, entity.CaptchaActionEmail, ctx.ClientIP(), req.CaptchaID, req.CaptchaCode) + if !captchaPass { + errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{ + ErrorField: "captcha_code", + ErrorMsg: translator.Tr(handler.GetLang(ctx), reason.CaptchaVerificationFailed), + }) + handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields) + return + } + } - captchaPass := uc.actionService.ActionRecordVerifyCaptcha(ctx, schema.ActionRecordTypeEmail, ctx.ClientIP(), - req.CaptchaID, req.CaptchaCode) - if !captchaPass { - errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{ - ErrorField: "captcha_code", - ErrorMsg: translator.Tr(handler.GetLang(ctx), reason.CaptchaVerificationFailed), - }) - handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields) - return - } - _, err := uc.actionService.ActionRecordAdd(ctx, schema.ActionRecordTypeEmail, ctx.ClientIP()) - if err != nil { - log.Error(err) - } - err = uc.userService.UserVerifyEmailSend(ctx, userInfo.UserID) + err := uc.userService.UserVerifyEmailSend(ctx, userInfo.UserID) handler.HandleResponse(ctx, err, nil) } @@ -349,20 +353,22 @@ func (uc *UserController) UserModifyPassWord(ctx *gin.Context) { } req.UserID = middleware.GetLoginUserIDFromContext(ctx) req.AccessToken = middleware.ExtractToken(ctx) - - captchaPass := uc.actionService.ActionRecordVerifyCaptcha(ctx, schema.ActionRecordTypeModifyPass, ctx.ClientIP(), - req.CaptchaID, req.CaptchaCode) - if !captchaPass { - errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{ - ErrorField: "captcha_code", - ErrorMsg: translator.Tr(handler.GetLang(ctx), reason.CaptchaVerificationFailed), - }) - handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields) - return - } - _, err := uc.actionService.ActionRecordAdd(ctx, schema.ActionRecordTypeModifyPass, ctx.ClientIP()) - if err != nil { - log.Error(err) + isAdmin := middleware.GetUserIsAdminModerator(ctx) + if !isAdmin { + captchaPass := uc.actionService.ActionRecordVerifyCaptcha(ctx, entity.CaptchaActionPassword, req.UserID, + req.CaptchaID, req.CaptchaCode) + if !captchaPass { + errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{ + ErrorField: "captcha_code", + ErrorMsg: translator.Tr(handler.GetLang(ctx), reason.CaptchaVerificationFailed), + }) + handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields) + return + } + _, err := uc.actionService.ActionRecordAdd(ctx, entity.CaptchaActionPassword, req.UserID) + if err != nil { + log.Error(err) + } } oldPassVerification, err := uc.userService.UserModifyPassWordVerification(ctx, req) @@ -389,7 +395,7 @@ func (uc *UserController) UserModifyPassWord(ctx *gin.Context) { } err = uc.userService.UserModifyPassword(ctx, req) if err == nil { - uc.actionService.ActionRecordDel(ctx, schema.ActionRecordTypeLogin, ctx.ClientIP()) + uc.actionService.ActionRecordDel(ctx, entity.CaptchaActionPassword, req.UserID) } handler.HandleResponse(ctx, err, nil) } @@ -452,10 +458,21 @@ func (uc *UserController) ActionRecord(ctx *gin.Context) { if handler.BindAndCheck(ctx, req) { return } + userinfo := middleware.GetUserInfoFromContext(ctx) + if userinfo != nil { + req.UserID = userinfo.UserID + } req.IP = ctx.ClientIP() + resp := &schema.ActionRecordResp{} + isAdmin := middleware.GetUserIsAdminModerator(ctx) + if isAdmin { + resp.Verify = false + handler.HandleResponse(ctx, nil, resp) + } else { + resp, err := uc.actionService.ActionRecord(ctx, req) + handler.HandleResponse(ctx, err, resp) + } - resp, err := uc.actionService.ActionRecord(ctx, req) - handler.HandleResponse(ctx, err, resp) } // UserRegisterCaptcha godoc @@ -523,22 +540,26 @@ func (uc *UserController) UserChangeEmailSendCode(ctx *gin.Context) { handler.HandleResponse(ctx, errors.BadRequest(reason.EmailIllegalDomainError), nil) return } - - captchaPass := uc.actionService.ActionRecordVerifyCaptcha(ctx, schema.ActionRecordTypeEmail, ctx.ClientIP(), req.CaptchaID, req.CaptchaCode) - if !captchaPass { - errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{ - ErrorField: "captcha_code", - ErrorMsg: translator.Tr(handler.GetLang(ctx), reason.CaptchaVerificationFailed), - }) - handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields) - return + isAdmin := middleware.GetUserIsAdminModerator(ctx) + if !isAdmin { + captchaPass := uc.actionService.ActionRecordVerifyCaptcha(ctx, entity.CaptchaActionPassword, req.UserID, req.CaptchaID, req.CaptchaCode) + if !captchaPass { + errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{ + ErrorField: "captcha_code", + ErrorMsg: translator.Tr(handler.GetLang(ctx), reason.CaptchaVerificationFailed), + }) + handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields) + return + } } - _, _ = uc.actionService.ActionRecordAdd(ctx, schema.ActionRecordTypeEmail, ctx.ClientIP()) resp, err := uc.userService.UserChangeEmailSendCode(ctx, req) if err != nil { handler.HandleResponse(ctx, err, resp) return } + if !isAdmin { + uc.actionService.ActionRecordAdd(ctx, entity.CaptchaActionPassword, req.UserID) + } handler.HandleResponse(ctx, err, nil) } @@ -565,7 +586,7 @@ func (uc *UserController) UserChangeEmailVerify(ctx *gin.Context) { } resp, err := uc.userService.UserChangeEmailVerify(ctx, req.Content) - uc.actionService.ActionRecordDel(ctx, schema.ActionRecordTypeEmail, ctx.ClientIP()) + uc.actionService.ActionRecordDel(ctx, entity.CaptchaActionEmail, ctx.ClientIP()) handler.HandleResponse(ctx, err, resp) } diff --git a/internal/controller/vote_controller.go b/internal/controller/vote_controller.go index 6af80e35..236fbe82 100644 --- a/internal/controller/vote_controller.go +++ b/internal/controller/vote_controller.go @@ -5,24 +5,35 @@ import ( "github.com/answerdev/answer/internal/base/middleware" "github.com/answerdev/answer/internal/base/reason" "github.com/answerdev/answer/internal/base/translator" + "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/action" "github.com/answerdev/answer/internal/service/rank" "github.com/answerdev/answer/pkg/uid" "github.com/gin-gonic/gin" - "github.com/jinzhu/copier" "github.com/segmentfault/pacman/errors" ) // VoteController activity controller type VoteController struct { - VoteService *service.VoteService - rankService *rank.RankService + VoteService *service.VoteService + rankService *rank.RankService + actionService *action.CaptchaService } // NewVoteController new controller -func NewVoteController(voteService *service.VoteService, rankService *rank.RankService) *VoteController { - return &VoteController{VoteService: voteService, rankService: rankService} +func NewVoteController( + voteService *service.VoteService, + rankService *rank.RankService, + actionService *action.CaptchaService, +) *VoteController { + return &VoteController{ + VoteService: voteService, + rankService: rankService, + actionService: actionService, + } } // VoteUp godoc @@ -42,6 +53,7 @@ func (vc *VoteController) VoteUp(ctx *gin.Context) { } req.ObjectID = uid.DeShortID(req.ObjectID) req.UserID = middleware.GetLoginUserIDFromContext(ctx) + can, needRank, err := vc.rankService.CheckVotePermission(ctx, req.UserID, req.ObjectID, true) if err != nil { handler.HandleResponse(ctx, err, nil) @@ -54,9 +66,23 @@ func (vc *VoteController) VoteUp(ctx *gin.Context) { return } - dto := &schema.VoteDTO{} - _ = copier.Copy(dto, req) - resp, err := vc.VoteService.VoteUp(ctx, dto) + isAdmin := middleware.GetUserIsAdminModerator(ctx) + if !isAdmin { + captchaPass := vc.actionService.ActionRecordVerifyCaptcha(ctx, entity.CaptchaActionVote, req.UserID, req.CaptchaID, req.CaptchaCode) + if !captchaPass { + errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{ + ErrorField: "captcha_code", + ErrorMsg: translator.Tr(handler.GetLang(ctx), reason.CaptchaVerificationFailed), + }) + handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields) + return + } + } + + if !isAdmin { + vc.actionService.ActionRecordAdd(ctx, entity.CaptchaActionVote, req.UserID) + } + resp, err := vc.VoteService.VoteUp(ctx, req) if err != nil { handler.HandleResponse(ctx, err, schema.ErrTypeToast) } else { @@ -81,6 +107,8 @@ func (vc *VoteController) VoteDown(ctx *gin.Context) { } req.ObjectID = uid.DeShortID(req.ObjectID) req.UserID = middleware.GetLoginUserIDFromContext(ctx) + isAdmin := middleware.GetUserIsAdminModerator(ctx) + can, needRank, err := vc.rankService.CheckVotePermission(ctx, req.UserID, req.ObjectID, false) if err != nil { handler.HandleResponse(ctx, err, nil) @@ -93,9 +121,21 @@ func (vc *VoteController) VoteDown(ctx *gin.Context) { return } - dto := &schema.VoteDTO{} - _ = copier.Copy(dto, req) - resp, err := vc.VoteService.VoteDown(ctx, dto) + if !isAdmin { + captchaPass := vc.actionService.ActionRecordVerifyCaptcha(ctx, entity.CaptchaActionVote, req.UserID, req.CaptchaID, req.CaptchaCode) + if !captchaPass { + errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{ + ErrorField: "captcha_code", + ErrorMsg: translator.Tr(handler.GetLang(ctx), reason.CaptchaVerificationFailed), + }) + handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields) + return + } + } + if !isAdmin { + vc.actionService.ActionRecordAdd(ctx, entity.CaptchaActionVote, req.UserID) + } + resp, err := vc.VoteService.VoteDown(ctx, req) if err != nil { handler.HandleResponse(ctx, err, schema.ErrTypeToast) } else { @@ -103,9 +143,9 @@ func (vc *VoteController) VoteDown(ctx *gin.Context) { } } -// UserVotes godoc -// @Summary user's votes -// @Description user's vote +// UserVotes user votes +// @Summary get user personal votes +// @Description get user personal votes // @Tags Activity // @Accept json // @Produce json @@ -116,21 +156,12 @@ func (vc *VoteController) VoteDown(ctx *gin.Context) { // @Router /answer/api/v1/personal/vote/page [get] func (vc *VoteController) UserVotes(ctx *gin.Context) { req := schema.GetVoteWithPageReq{} - req.UserID = middleware.GetLoginUserIDFromContext(ctx) if handler.BindAndCheck(ctx, &req) { return } - if req.Page == 0 { - req.Page = 1 - } - if req.PageSize == 0 { - req.PageSize = 30 - } + + req.UserID = middleware.GetLoginUserIDFromContext(ctx) resp, err := vc.VoteService.ListUserVotes(ctx, req) - if err != nil { - handler.HandleResponse(ctx, err, schema.ErrTypeModal) - } else { - handler.HandleResponse(ctx, err, resp) - } + handler.HandleResponse(ctx, err, resp) } diff --git a/internal/controller_admin/user_backyard_controller.go b/internal/controller_admin/user_backyard_controller.go index cb8fe7d8..8e377757 100644 --- a/internal/controller_admin/user_backyard_controller.go +++ b/internal/controller_admin/user_backyard_controller.go @@ -135,3 +135,41 @@ func (uc *UserAdminController) GetUserPage(ctx *gin.Context) { resp, err := uc.userService.GetUserPage(ctx, req) handler.HandleResponse(ctx, err, resp) } + +// GetUserActivation get user activation +// @Summary get user activation +// @Description get user activation +// @Security ApiKeyAuth +// @Tags admin +// @Produce json +// @Param user_id query string true "user id" +// @Success 200 {object} handler.RespBody{data=schema.GetUserActivationResp} +// @Router /answer/admin/api/user/activation [get] +func (uc *UserAdminController) GetUserActivation(ctx *gin.Context) { + req := &schema.GetUserActivationReq{} + if handler.BindAndCheck(ctx, req) { + return + } + + resp, err := uc.userService.GetUserActivation(ctx, req) + handler.HandleResponse(ctx, err, resp) +} + +// SendUserActivation send user activation +// @Summary send user activation +// @Description send user activation +// @Security ApiKeyAuth +// @Tags admin +// @Produce json +// @Param data body schema.SendUserActivationReq true "SendUserActivationReq" +// @Success 200 {object} handler.RespBody +// @Router /answer/admin/api/users/activation [post] +func (uc *UserAdminController) SendUserActivation(ctx *gin.Context) { + req := &schema.SendUserActivationReq{} + if handler.BindAndCheck(ctx, req) { + return + } + + err := uc.userService.SendUserActivation(ctx, req) + handler.HandleResponse(ctx, err, nil) +} diff --git a/internal/entity/answer_entity.go b/internal/entity/answer_entity.go index 43567efd..74b599dc 100644 --- a/internal/entity/answer_entity.go +++ b/internal/entity/answer_entity.go @@ -42,15 +42,6 @@ type AnswerSearch struct { PageSize int `json:"page_size" form:"page_size"` // Search page size } -type AdminAnswerSearch struct { - Page int `json:"page" form:"page"` // Query number of pages - PageSize int `json:"page_size" form:"page_size"` // Search page size - Status int `json:"-" form:"-"` - StatusStr string `json:"status" form:"status"` // Status 1 Available 2 closed 10 Deleted - Query string `validate:"omitempty,gt=0,lte=100" json:"query" form:"query" ` //Query string - QuestionID string `validate:"omitempty,gt=0,lte=24" json:"question_id" form:"question_id" ` //Query string -} - // TableName answer table name func (Answer) TableName() string { return "answer" diff --git a/internal/entity/captcha_entity.go b/internal/entity/captcha_entity.go new file mode 100644 index 00000000..1de095c4 --- /dev/null +++ b/internal/entity/captcha_entity.go @@ -0,0 +1,22 @@ +package entity + +const ( + CaptchaActionEmail = "email" + CaptchaActionPassword = "password" + CaptchaActionEditUserinfo = "edit_userinfo" + CaptchaActionQuestion = "question" + CaptchaActionAnswer = "answer" + CaptchaActionComment = "comment" + CaptchaActionEdit = "edit" + CaptchaActionInvitationAnswer = "invitation_answer" + CaptchaActionSearch = "search" + CaptchaActionReport = "report" + CaptchaActionDelete = "delete" + CaptchaActionVote = "vote" +) + +type ActionRecordInfo struct { + LastTime int64 `json:"last_time"` + Num int `json:"num"` + Config string `json:"config"` +} diff --git a/internal/entity/revision_entity.go b/internal/entity/revision_entity.go index 34edc7f8..584a823e 100644 --- a/internal/entity/revision_entity.go +++ b/internal/entity/revision_entity.go @@ -24,7 +24,7 @@ type Revision struct { ObjectType int `xorm:"not null default 0 INT(11) object_type"` ObjectID string `xorm:"not null default 0 BIGINT(20) INDEX object_id"` Title string `xorm:"not null default '' VARCHAR(255) title"` - Content string `xorm:"not null TEXT content"` + Content string `xorm:"not null MEDIUMTEXT content"` Log string `xorm:"VARCHAR(255) log"` Status int `xorm:"not null default 1 INT(11) status"` ReviewUserID int64 `xorm:"not null default 0 BIGINT(20) review_user_id"` diff --git a/internal/install/install_controller.go b/internal/install/install_controller.go index 465378f7..154976c2 100644 --- a/internal/install/install_controller.go +++ b/internal/install/install_controller.go @@ -16,6 +16,7 @@ import ( "github.com/answerdev/answer/internal/migrations" "github.com/answerdev/answer/internal/schema" "github.com/gin-gonic/gin" + "github.com/jinzhu/copier" "github.com/segmentfault/pacman/errors" "github.com/segmentfault/pacman/log" ) @@ -184,17 +185,17 @@ func InitBaseInfo(ctx *gin.Context) { return } - if err := migrations.InitDB(c.Data.Database); err != nil { - log.Error("init database error: ", err.Error()) - handler.HandleResponse(ctx, errors.BadRequest(reason.InstallCreateTableFailed), schema.ErrTypeAlert) - return + engine, err := data.NewDB(false, c.Data.Database) + if err != nil { + log.Errorf("init database failed %s", err) + handler.HandleResponse(ctx, errors.BadRequest(reason.InstallCreateTableFailed), nil) } - err = migrations.UpdateInstallInfo(c.Data.Database, req.Language, req.SiteName, req.SiteURL, req.ContactEmail, - req.AdminName, req.AdminPassword, req.AdminEmail) - if err != nil { - log.Error(err) - handler.HandleResponse(ctx, errors.BadRequest(reason.InstallConfigFailed), nil) + inputData := &migrations.InitNeedUserInputData{} + _ = copier.Copy(inputData, req) + if err := migrations.NewMentor(ctx, engine, inputData).InitDB(); err != nil { + log.Error("init database error: ", err.Error()) + handler.HandleResponse(ctx, errors.BadRequest(reason.InstallConfigFailed), schema.ErrTypeAlert) return } diff --git a/internal/migrations/init.go b/internal/migrations/init.go index 0df92b56..0dc3b35d 100644 --- a/internal/migrations/init.go +++ b/internal/migrations/init.go @@ -1,188 +1,200 @@ package migrations import ( + "context" "encoding/json" "fmt" + "github.com/answerdev/answer/internal/schema" + "github.com/segmentfault/pacman/log" - "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{}, - &entity.Collection{}, - &entity.CollectionGroup{}, - &entity.Comment{}, - &entity.Config{}, - &entity.Meta{}, - &entity.Notification{}, - &entity.Question{}, - &entity.Report{}, - &entity.Revision{}, - &entity.SiteInfo{}, - &entity.Tag{}, - &entity.TagRel{}, - &entity.Uniqid{}, - &entity.User{}, - &entity.Version{}, - &entity.Role{}, - &entity.RolePowerRel{}, - &entity.Power{}, - &entity.UserRoleRel{}, - &entity.PluginConfig{}, - &entity.UserExternalLogin{}, +type Mentor struct { + ctx context.Context + engine *xorm.Engine + userData *InitNeedUserInputData + err error + Done bool } -// InitDB init db -func InitDB(dataConf *data.Database) (err error) { - engine, err := data.NewDB(false, dataConf) - if err != nil { - fmt.Println("new database failed: ", err.Error()) - return err - } +func NewMentor(ctx context.Context, engine *xorm.Engine, data *InitNeedUserInputData) *Mentor { + return &Mentor{ctx: ctx, engine: engine, userData: data} +} - exist, err := engine.IsTableExist(&entity.Version{}) - if err != nil { - return fmt.Errorf("check table exists failed: %s", err) +type InitNeedUserInputData struct { + Language string + SiteName string + SiteURL string + ContactEmail string + AdminName string + AdminPassword string + AdminEmail string +} + +func (m *Mentor) InitDB() error { + m.do("check table exist", m.checkTableExist) + m.do("sync table", m.syncTable) + m.do("init version table", m.initVersionTable) + m.do("init admin user", m.initAdminUser) + m.do("init config", m.initConfig) + m.do("init default privileges config", m.initDefaultRankPrivileges) + m.do("init role", m.initRole) + m.do("init power", m.initPower) + m.do("init role power rel", m.initRolePowerRel) + m.do("init admin user role rel", m.initAdminUserRoleRel) + m.do("init site info interface", m.initSiteInfoInterface) + m.do("init site info general config", m.initSiteInfoGeneralData) + m.do("init site info login config", m.initSiteInfoLoginConfig) + m.do("init site info theme config", m.initSiteInfoThemeConfig) + m.do("init site info seo config", m.initSiteInfoSEOConfig) + m.do("init site info user config", m.initSiteInfoUsersConfig) + m.do("init site info privilege rank", m.initSiteInfoPrivilegeRank) + return m.err +} + +func (m *Mentor) do(taskName string, fn func()) { + if m.err != nil || m.Done { + return } - if exist { + fn() + if m.err != nil { + m.err = fmt.Errorf("%s failed: %s", taskName, m.err) + } +} + +func (m *Mentor) checkTableExist() { + m.Done, m.err = m.engine.Context(m.ctx).IsTableExist(&entity.Version{}) + if m.Done { fmt.Println("[database] already exists") - return nil } - - err = engine.Sync(tables...) - 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 { - return fmt.Errorf("init admin user failed: %s", err) - } - - err = initConfigTable(engine) - 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{ +func (m *Mentor) syncTable() { + m.err = m.engine.Context(m.ctx).Sync(tables...) +} + +func (m *Mentor) initVersionTable() { + _, m.err = m.engine.Context(m.ctx).Insert(&entity.Version{ID: 1, VersionNumber: ExpectedVersion()}) +} + +func (m *Mentor) initAdminUser() { + generateFromPassword, _ := bcrypt.GenerateFromPassword([]byte(m.userData.AdminPassword), bcrypt.DefaultCost) + _, m.err = m.engine.Context(m.ctx).Insert(&entity.User{ ID: "1", - Username: "admin", - Pass: "$2a$10$.gnUnpW.8ssRNaEvx.XwvOR2NuPsGzFLWWX2rqSIVAdIvLNZZYs5y", // admin - EMail: "admin@admin.com", + Username: m.userData.AdminName, + Pass: string(generateFromPassword), + EMail: m.userData.AdminEmail, MailStatus: 1, NoticeStatus: 1, Status: 1, Rank: 1, - DisplayName: "admin", + DisplayName: m.userData.AdminName, }) - return err } -func initSiteInfo(engine *xorm.Engine, language, siteName, siteURL, contactEmail string) error { +func (m *Mentor) initConfig() { + _, m.err = m.engine.Context(m.ctx).Insert(defaultConfigTable) +} + +func (m *Mentor) initDefaultRankPrivileges() { + chooseOption := schema.DefaultPrivilegeOptions.Choose(schema.PrivilegeLevel2) + for _, privilege := range chooseOption.Privileges { + _, err := m.engine.Context(m.ctx).Update( + &entity.Config{Value: fmt.Sprintf("%d", privilege.Value)}, + &entity.Config{Key: privilege.Key}, + ) + if err != nil { + log.Error(err) + } + } +} + +func (m *Mentor) initRole() { + _, m.err = m.engine.Context(m.ctx).Insert(roles) +} + +func (m *Mentor) initPower() { + _, m.err = m.engine.Context(m.ctx).Insert(powers) +} + +func (m *Mentor) initRolePowerRel() { + _, m.err = m.engine.Context(m.ctx).Insert(rolePowerRels) +} + +func (m *Mentor) initAdminUserRoleRel() { + _, m.err = m.engine.Context(m.ctx).Insert(adminUserRoleRel) +} + +func (m *Mentor) initSiteInfoInterface() { interfaceData := map[string]string{ - "language": language, + "language": m.userData.Language, "time_zone": "UTC", } interfaceDataBytes, _ := json.Marshal(interfaceData) - _, err := engine.InsertOne(&entity.SiteInfo{ + _, m.err = m.engine.Context(m.ctx).Insert(&entity.SiteInfo{ Type: "interface", Content: string(interfaceDataBytes), Status: 1, }) - if err != nil { - return err - } +} +func (m *Mentor) initSiteInfoGeneralData() { generalData := map[string]string{ - "name": siteName, - "site_url": siteURL, - "contact_email": contactEmail, + "name": m.userData.SiteName, + "site_url": m.userData.SiteURL, + "contact_email": m.userData.ContactEmail, } generalDataBytes, _ := json.Marshal(generalData) - _, err = engine.InsertOne(&entity.SiteInfo{ + _, m.err = m.engine.Context(m.ctx).Insert(&entity.SiteInfo{ Type: "general", Content: string(generalDataBytes), Status: 1, }) - if err != nil { - return err - } +} +func (m *Mentor) initSiteInfoLoginConfig() { loginConfig := map[string]bool{ "allow_new_registrations": true, "allow_email_registrations": true, "login_required": false, } loginConfigDataBytes, _ := json.Marshal(loginConfig) - _, err = engine.InsertOne(&entity.SiteInfo{ + _, m.err = m.engine.Context(m.ctx).Insert(&entity.SiteInfo{ Type: "login", Content: string(loginConfigDataBytes), Status: 1, }) - if err != nil { - return err - } +} +func (m *Mentor) initSiteInfoThemeConfig() { themeConfig := `{"theme":"default","theme_config":{"default":{"navbar_style":"colored","primary_color":"#0033ff"}}}` - _, err = engine.InsertOne(&entity.SiteInfo{ + _, m.err = m.engine.Context(m.ctx).Insert(&entity.SiteInfo{ Type: "theme", Content: themeConfig, Status: 1, }) - if err != nil { - return err - } +} - seoData := map[string]string{ - "robots": defaultSEORobotTxt + siteURL + "/sitemap.xml", +func (m *Mentor) initSiteInfoSEOConfig() { + seoData := map[string]interface{}{ + "permalink": 1, + "robots": defaultSEORobotTxt + m.userData.SiteURL + "/sitemap.xml", } seoDataBytes, _ := json.Marshal(seoData) - _, err = engine.InsertOne(&entity.SiteInfo{ + _, m.err = m.engine.Context(m.ctx).Insert(&entity.SiteInfo{ Type: "seo", Content: string(seoDataBytes), Status: 1, }) - if err != nil { - return err - } +} +func (m *Mentor) initSiteInfoUsersConfig() { usersData := map[string]any{ "default_avatar": "gravatar", - "default_gravatar_base_url": "https://www.gravatar.com/avatar/", + "gravatar_base_url": "https://www.gravatar.com/avatar/", "allow_update_display_name": true, "allow_update_username": true, "allow_update_avatar": true, @@ -191,344 +203,21 @@ func initSiteInfo(engine *xorm.Engine, language, siteName, siteURL, contactEmail "allow_update_location": true, } usersDataBytes, _ := json.Marshal(usersData) - _, err = engine.InsertOne(&entity.SiteInfo{ + _, m.err = m.engine.Context(m.ctx).Insert(&entity.SiteInfo{ Type: "users", Content: string(usersDataBytes), 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 err +func (m *Mentor) initSiteInfoPrivilegeRank() { + privilegeRankData := map[string]interface{}{ + "level": schema.PrivilegeLevel2, } - adminPassword = string(generateFromPassword) - - // update admin info - _, err = engine.ID("1").Update(&entity.User{ - Username: adminName, - Pass: adminPassword, - EMail: adminEmail, - DisplayName: adminName, + privilegeRankDataBytes, _ := json.Marshal(privilegeRankData) + _, m.err = m.engine.Context(m.ctx).Insert(&entity.SiteInfo{ + Type: "privileges", + Content: string(privilegeRankDataBytes), + Status: 1, }) - if err != nil { - return fmt.Errorf("update admin user info failed: %s", err) - } - return nil -} - -// UpdateInstallInfo update some init data about the admin interface and admin password -func UpdateInstallInfo(dataConf *data.Database, language string, - siteName string, - siteURL string, - contactEmail string, - adminName string, - adminPassword string, - adminEmail string) error { - - engine, err := data.NewDB(false, dataConf) - if err != nil { - return fmt.Errorf("database connection error: %s", err) - } - - err = updateAdminInfo(engine, adminName, adminPassword, adminEmail) - if err != nil { - return fmt.Errorf("update admin info failed: %s", err) - } - - err = initSiteInfo(engine, language, siteName, siteURL, contactEmail) - if err != nil { - return fmt.Errorf("init site info failed: %s", err) - } - return err -} - -func initConfigTable(engine *xorm.Engine) error { - defaultConfigTable := []*entity.Config{ - {ID: 1, Key: "answer.accepted", Value: `15`}, - {ID: 2, Key: "answer.voted_up", Value: `10`}, - {ID: 3, Key: "question.voted_up", Value: `10`}, - {ID: 4, Key: "tag.edit_accepted", Value: `2`}, - {ID: 5, Key: "answer.accept", Value: `2`}, - {ID: 6, Key: "answer.voted_down_cancel", Value: `2`}, - {ID: 7, Key: "question.voted_down_cancel", Value: `2`}, - {ID: 8, Key: "answer.vote_down_cancel", Value: `1`}, - {ID: 9, Key: "question.vote_down_cancel", Value: `1`}, - {ID: 10, Key: "user.activated", Value: `1`}, - {ID: 11, Key: "edit.accepted", Value: `2`}, - {ID: 12, Key: "answer.vote_down", Value: `-1`}, - {ID: 13, Key: "question.voted_down", Value: `-2`}, - {ID: 14, Key: "answer.voted_down", Value: `-2`}, - {ID: 15, Key: "answer.accept_cancel", Value: `-2`}, - {ID: 16, Key: "answer.deleted", Value: `-5`}, - {ID: 17, Key: "question.voted_up_cancel", Value: `-10`}, - {ID: 18, Key: "answer.voted_up_cancel", Value: `-10`}, - {ID: 19, Key: "answer.accepted_cancel", Value: `-15`}, - {ID: 20, Key: "object.reported", Value: `-100`}, - {ID: 21, Key: "edit.rejected", Value: `-2`}, - {ID: 22, Key: "daily_rank_limit", Value: `200`}, - {ID: 23, Key: "daily_rank_limit.exclude", Value: `["answer.accepted"]`}, - {ID: 24, Key: "user.follow", Value: `0`}, - {ID: 25, Key: "comment.vote_up", Value: `0`}, - {ID: 26, Key: "comment.vote_up_cancel", Value: `0`}, - {ID: 27, Key: "question.vote_down", Value: `0`}, - {ID: 28, Key: "question.vote_up", Value: `0`}, - {ID: 29, Key: "question.vote_up_cancel", Value: `0`}, - {ID: 30, Key: "answer.vote_up", Value: `0`}, - {ID: 31, Key: "answer.vote_up_cancel", Value: `0`}, - {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.","new_answer_title":"[{{.SiteName}}] {{.DisplayName}} answered your question","new_answer_body":"{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.AnswerSummary}}

\nView it on {{.SiteName}}

\n\nYou are receiving this because you authored the thread. Unsubscribe","new_comment_title":"[{{.SiteName}}] {{.DisplayName}} commented on your post","new_comment_body":"{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.CommentSummary}}

\nView it on {{.SiteName}}

\n\nYou are receiving this because you authored the thread. Unsubscribe"}`}, - {ID: 35, Key: "tag.follow", 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."}`}, - {ID: 59, Key: "reason.something", Value: `{"name":"something else","description":"This post requires staff attention for another reason not listed above.","content_type":"textarea"}`}, - {ID: 60, Key: "reason.a_duplicate", Value: `{"name":"a duplicate","description":"This question has been asked before and already has an answer.","content_type":"text"}`}, - {ID: 61, Key: "reason.not_a_answer", Value: `{"name":"not a answer","description":"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.","content_type":""}`}, - {ID: 62, Key: "reason.no_longer_needed", Value: `{"name":"no longer needed","description":"This comment is outdated, conversational or not relevant to this post."}`}, - {ID: 63, Key: "reason.community_specific", Value: `{"name":"a community-specific reason","description":"This question doesn’t meet a community guideline."}`}, - {ID: 64, Key: "reason.not_clarity", Value: `{"name":"needs details or clarity","description":"This question currently includes multiple questions in one. It should focus on one problem only.","content_type":"text"}`}, - {ID: 65, Key: "reason.normal", Value: `{"name":"normal","description":"A normal post available to everyone."}`}, - {ID: 66, Key: "reason.normal.user", Value: `{"name":"normal","description":"A normal user can ask and answer questions."}`}, - {ID: 67, Key: "reason.closed", Value: `{"name":"closed","description":"A closed question can’t answer, but still can edit, vote and comment."}`}, - {ID: 68, Key: "reason.deleted", Value: `{"name":"deleted","description":"All reputation gained and lost will be restored."}`}, - {ID: 69, Key: "reason.deleted.user", Value: `{"name":"deleted","description":"Delete profile, authentication associations."}`}, - {ID: 70, Key: "reason.suspended", Value: `{"name":"suspended","description":"A suspended user can’t log in."}`}, - {ID: 71, Key: "reason.inactive", Value: `{"name":"inactive","description":"An inactive user must re-validate their email."}`}, - {ID: 72, Key: "reason.looks_ok", Value: `{"name":"looks ok","description":"This post is good as-is and not low quality."}`}, - {ID: 73, Key: "reason.needs_edit", Value: `{"name":"needs edit, and I did it","description":"Improve and correct problems with this post yourself."}`}, - {ID: 74, Key: "reason.needs_close", Value: `{"name":"needs close","description":"A closed question can’t answer, but still can edit, vote and comment."}`}, - {ID: 75, Key: "reason.needs_delete", Value: `{"name":"needs delete","description":"All reputation gained and lost will be restored."}`}, - {ID: 76, Key: "question.flag.reasons", Value: `["reason.spam","reason.rude_or_abusive","reason.something","reason.a_duplicate"]`}, - {ID: 77, Key: "answer.flag.reasons", Value: `["reason.spam","reason.rude_or_abusive","reason.something","reason.not_a_answer"]`}, - {ID: 78, Key: "comment.flag.reasons", Value: `["reason.spam","reason.rude_or_abusive","reason.something","reason.no_longer_needed"]`}, - {ID: 79, Key: "question.close.reasons", Value: `["reason.a_duplicate","reason.community_specific","reason.not_clarity","reason.something"]`}, - {ID: 80, Key: "question.status.reasons", Value: `["reason.normal","reason.closed","reason.deleted"]`}, - {ID: 81, Key: "answer.status.reasons", Value: `["reason.normal","reason.deleted"]`}, - {ID: 82, Key: "comment.status.reasons", Value: `["reason.normal","reason.deleted"]`}, - {ID: 83, Key: "user.status.reasons", Value: `["reason.normal.user","reason.suspended","reason.deleted.user","reason.inactive"]`}, - {ID: 84, Key: "question.review.reasons", Value: `["reason.looks_ok","reason.needs_edit","reason.needs_close","reason.needs_delete"]`}, - {ID: 85, Key: "answer.review.reasons", Value: `["reason.looks_ok","reason.needs_edit","reason.needs_delete"]`}, - {ID: 86, Key: "comment.review.reasons", Value: `["reason.looks_ok","reason.needs_edit","reason.needs_delete"]`}, - {ID: 87, Key: "question.asked", Value: `0`}, - {ID: 88, Key: "question.closed", Value: `0`}, - {ID: 89, Key: "question.reopened", Value: `0`}, - {ID: 90, Key: "question.answered", Value: `0`}, - {ID: 91, Key: "question.commented", Value: `0`}, - {ID: 92, Key: "question.accept", Value: `0`}, - {ID: 93, Key: "question.edited", Value: `0`}, - {ID: 94, Key: "question.rollback", Value: `0`}, - {ID: 95, Key: "question.deleted", Value: `0`}, - {ID: 96, Key: "question.undeleted", Value: `0`}, - {ID: 97, Key: "answer.answered", Value: `0`}, - {ID: 98, Key: "answer.commented", Value: `0`}, - {ID: 99, Key: "answer.edited", Value: `0`}, - {ID: 100, Key: "answer.rollback", Value: `0`}, - {ID: 101, Key: "answer.undeleted", Value: `0`}, - {ID: 102, Key: "tag.created", Value: `0`}, - {ID: 103, Key: "tag.edited", Value: `0`}, - {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`}, - {ID: 118, Key: "plugin.status", Value: `{}`}, - {ID: 119, Key: "question.pin", Value: `0`}, - {ID: 120, Key: "question.unpin", Value: `0`}, - {ID: 121, Key: "question.show", Value: `0`}, - {ID: 122, Key: "question.hide", Value: `0`}, - {ID: 123, Key: "rank.question.pin", Value: `-1`}, - {ID: 124, Key: "rank.question.unpin", Value: `-1`}, - {ID: 125, Key: "rank.question.show", Value: `-1`}, - {ID: 126, Key: "rank.question.hide", Value: `-1`}, - {ID: 127, Key: "rank.answer.invite_someone_to_answer", Value: `1000`}, - } - _, 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"}, - {ID: 34, Name: "question pin", PowerType: permission.QuestionPin, Description: "top the question"}, - {ID: 35, Name: "question hide", PowerType: permission.QuestionHide, Description: "hide the question"}, - {ID: 36, Name: "question unpin", PowerType: permission.QuestionUnPin, Description: "untop the question"}, - {ID: 37, Name: "question show", PowerType: permission.QuestionShow, Description: "show the question"}, - {ID: 38, Name: "invite someone to answer", PowerType: permission.AnswerInviteSomeoneToAnswer, Description: "invite someone to answer"}, - } - _, 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: 2, PowerType: permission.QuestionPin}, - {RoleID: 2, PowerType: permission.QuestionHide}, - {RoleID: 2, PowerType: permission.QuestionUnPin}, - {RoleID: 2, PowerType: permission.QuestionShow}, - {RoleID: 2, PowerType: permission.AnswerInviteSomeoneToAnswer}, - - {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}, - {RoleID: 3, PowerType: permission.QuestionPin}, - {RoleID: 3, PowerType: permission.QuestionHide}, - {RoleID: 3, PowerType: permission.QuestionUnPin}, - {RoleID: 3, PowerType: permission.QuestionShow}, - {RoleID: 3, PowerType: permission.AnswerInviteSomeoneToAnswer}, - } - _, 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/init_data.go b/internal/migrations/init_data.go new file mode 100644 index 00000000..fc2d8aa8 --- /dev/null +++ b/internal/migrations/init_data.go @@ -0,0 +1,313 @@ +package migrations + +import ( + "github.com/answerdev/answer/internal/entity" + "github.com/answerdev/answer/internal/service/permission" +) + +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{}, + &entity.Collection{}, + &entity.CollectionGroup{}, + &entity.Comment{}, + &entity.Config{}, + &entity.Meta{}, + &entity.Notification{}, + &entity.Question{}, + &entity.Report{}, + &entity.Revision{}, + &entity.SiteInfo{}, + &entity.Tag{}, + &entity.TagRel{}, + &entity.Uniqid{}, + &entity.User{}, + &entity.Version{}, + &entity.Role{}, + &entity.RolePowerRel{}, + &entity.Power{}, + &entity.UserRoleRel{}, + &entity.PluginConfig{}, + &entity.UserExternalLogin{}, + } + + 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."}, + } + + 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"}, + {ID: 34, Name: "question pin", PowerType: permission.QuestionPin, Description: "top the question"}, + {ID: 35, Name: "question hide", PowerType: permission.QuestionHide, Description: "hide the question"}, + {ID: 36, Name: "question unpin", PowerType: permission.QuestionUnPin, Description: "untop the question"}, + {ID: 37, Name: "question show", PowerType: permission.QuestionShow, Description: "show the question"}, + {ID: 38, Name: "invite someone to answer", PowerType: permission.AnswerInviteSomeoneToAnswer, Description: "invite someone to answer"}, + } + + 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: 2, PowerType: permission.QuestionPin}, + {RoleID: 2, PowerType: permission.QuestionHide}, + {RoleID: 2, PowerType: permission.QuestionUnPin}, + {RoleID: 2, PowerType: permission.QuestionShow}, + {RoleID: 2, PowerType: permission.AnswerInviteSomeoneToAnswer}, + + {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}, + {RoleID: 3, PowerType: permission.QuestionPin}, + {RoleID: 3, PowerType: permission.QuestionHide}, + {RoleID: 3, PowerType: permission.QuestionUnPin}, + {RoleID: 3, PowerType: permission.QuestionShow}, + {RoleID: 3, PowerType: permission.AnswerInviteSomeoneToAnswer}, + } + + adminUserRoleRel = &entity.UserRoleRel{ + UserID: "1", + RoleID: 2, + } + + defaultConfigTable = []*entity.Config{ + {ID: 1, Key: "answer.accepted", Value: `15`}, + {ID: 2, Key: "answer.voted_up", Value: `10`}, + {ID: 3, Key: "question.voted_up", Value: `10`}, + {ID: 4, Key: "tag.edit_accepted", Value: `2`}, + {ID: 5, Key: "answer.accept", Value: `2`}, + {ID: 6, Key: "answer.voted_down_cancel", Value: `2`}, + {ID: 7, Key: "question.voted_down_cancel", Value: `2`}, + {ID: 8, Key: "answer.vote_down_cancel", Value: `1`}, + {ID: 9, Key: "question.vote_down_cancel", Value: `1`}, + {ID: 10, Key: "user.activated", Value: `1`}, + {ID: 11, Key: "edit.accepted", Value: `2`}, + {ID: 12, Key: "answer.vote_down", Value: `-1`}, + {ID: 13, Key: "question.voted_down", Value: `-2`}, + {ID: 14, Key: "answer.voted_down", Value: `-2`}, + {ID: 15, Key: "answer.accept_cancel", Value: `-2`}, + {ID: 16, Key: "answer.deleted", Value: `-5`}, + {ID: 17, Key: "question.voted_up_cancel", Value: `-10`}, + {ID: 18, Key: "answer.voted_up_cancel", Value: `-10`}, + {ID: 19, Key: "answer.accepted_cancel", Value: `-15`}, + {ID: 20, Key: "object.reported", Value: `-100`}, + {ID: 21, Key: "edit.rejected", Value: `-2`}, + {ID: 22, Key: "daily_rank_limit", Value: `200`}, + {ID: 23, Key: "daily_rank_limit.exclude", Value: `["answer.accepted"]`}, + {ID: 24, Key: "user.follow", Value: `0`}, + {ID: 25, Key: "comment.vote_up", Value: `0`}, + {ID: 26, Key: "comment.vote_up_cancel", Value: `0`}, + {ID: 27, Key: "question.vote_down", Value: `0`}, + {ID: 28, Key: "question.vote_up", Value: `0`}, + {ID: 29, Key: "question.vote_up_cancel", Value: `0`}, + {ID: 30, Key: "answer.vote_up", Value: `0`}, + {ID: 31, Key: "answer.vote_up_cancel", Value: `0`}, + {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.","new_answer_title":"[{{.SiteName}}] {{.DisplayName}} answered your question","new_answer_body":"{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.AnswerSummary}}

\nView it on {{.SiteName}}

\n\nYou are receiving this because you authored the thread. Unsubscribe","new_comment_title":"[{{.SiteName}}] {{.DisplayName}} commented on your post","new_comment_body":"{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.CommentSummary}}

\nView it on {{.SiteName}}

\n\nYou are receiving this because you authored the thread. Unsubscribe"}`}, + {ID: 35, Key: "tag.follow", 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: `1500`}, + {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."}`}, + {ID: 59, Key: "reason.something", Value: `{"name":"something else","description":"This post requires staff attention for another reason not listed above.","content_type":"textarea"}`}, + {ID: 60, Key: "reason.a_duplicate", Value: `{"name":"a duplicate","description":"This question has been asked before and already has an answer.","content_type":"text"}`}, + {ID: 61, Key: "reason.not_a_answer", Value: `{"name":"not a answer","description":"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.","content_type":""}`}, + {ID: 62, Key: "reason.no_longer_needed", Value: `{"name":"no longer needed","description":"This comment is outdated, conversational or not relevant to this post."}`}, + {ID: 63, Key: "reason.community_specific", Value: `{"name":"a community-specific reason","description":"This question doesn’t meet a community guideline."}`}, + {ID: 64, Key: "reason.not_clarity", Value: `{"name":"needs details or clarity","description":"This question currently includes multiple questions in one. It should focus on one problem only.","content_type":"text"}`}, + {ID: 65, Key: "reason.normal", Value: `{"name":"normal","description":"A normal post available to everyone."}`}, + {ID: 66, Key: "reason.normal.user", Value: `{"name":"normal","description":"A normal user can ask and answer questions."}`}, + {ID: 67, Key: "reason.closed", Value: `{"name":"closed","description":"A closed question can’t answer, but still can edit, vote and comment."}`}, + {ID: 68, Key: "reason.deleted", Value: `{"name":"deleted","description":"All reputation gained and lost will be restored."}`}, + {ID: 69, Key: "reason.deleted.user", Value: `{"name":"deleted","description":"Delete profile, authentication associations."}`}, + {ID: 70, Key: "reason.suspended", Value: `{"name":"suspended","description":"A suspended user can’t log in."}`}, + {ID: 71, Key: "reason.inactive", Value: `{"name":"inactive","description":"An inactive user must re-validate their email."}`}, + {ID: 72, Key: "reason.looks_ok", Value: `{"name":"looks ok","description":"This post is good as-is and not low quality."}`}, + {ID: 73, Key: "reason.needs_edit", Value: `{"name":"needs edit, and I did it","description":"Improve and correct problems with this post yourself."}`}, + {ID: 74, Key: "reason.needs_close", Value: `{"name":"needs close","description":"A closed question can’t answer, but still can edit, vote and comment."}`}, + {ID: 75, Key: "reason.needs_delete", Value: `{"name":"needs delete","description":"All reputation gained and lost will be restored."}`}, + {ID: 76, Key: "question.flag.reasons", Value: `["reason.spam","reason.rude_or_abusive","reason.something","reason.a_duplicate"]`}, + {ID: 77, Key: "answer.flag.reasons", Value: `["reason.spam","reason.rude_or_abusive","reason.something","reason.not_a_answer"]`}, + {ID: 78, Key: "comment.flag.reasons", Value: `["reason.spam","reason.rude_or_abusive","reason.something","reason.no_longer_needed"]`}, + {ID: 79, Key: "question.close.reasons", Value: `["reason.a_duplicate","reason.community_specific","reason.not_clarity","reason.something"]`}, + {ID: 80, Key: "question.status.reasons", Value: `["reason.normal","reason.closed","reason.deleted"]`}, + {ID: 81, Key: "answer.status.reasons", Value: `["reason.normal","reason.deleted"]`}, + {ID: 82, Key: "comment.status.reasons", Value: `["reason.normal","reason.deleted"]`}, + {ID: 83, Key: "user.status.reasons", Value: `["reason.normal.user","reason.suspended","reason.deleted.user","reason.inactive"]`}, + {ID: 84, Key: "question.review.reasons", Value: `["reason.looks_ok","reason.needs_edit","reason.needs_close","reason.needs_delete"]`}, + {ID: 85, Key: "answer.review.reasons", Value: `["reason.looks_ok","reason.needs_edit","reason.needs_delete"]`}, + {ID: 86, Key: "comment.review.reasons", Value: `["reason.looks_ok","reason.needs_edit","reason.needs_delete"]`}, + {ID: 87, Key: "question.asked", Value: `0`}, + {ID: 88, Key: "question.closed", Value: `0`}, + {ID: 89, Key: "question.reopened", Value: `0`}, + {ID: 90, Key: "question.answered", Value: `0`}, + {ID: 91, Key: "question.commented", Value: `0`}, + {ID: 92, Key: "question.accept", Value: `0`}, + {ID: 93, Key: "question.edited", Value: `0`}, + {ID: 94, Key: "question.rollback", Value: `0`}, + {ID: 95, Key: "question.deleted", Value: `0`}, + {ID: 96, Key: "question.undeleted", Value: `0`}, + {ID: 97, Key: "answer.answered", Value: `0`}, + {ID: 98, Key: "answer.commented", Value: `0`}, + {ID: 99, Key: "answer.edited", Value: `0`}, + {ID: 100, Key: "answer.rollback", Value: `0`}, + {ID: 101, Key: "answer.undeleted", Value: `0`}, + {ID: 102, Key: "tag.created", Value: `0`}, + {ID: 103, Key: "tag.edited", Value: `0`}, + {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`}, + {ID: 118, Key: "plugin.status", Value: `{}`}, + {ID: 119, Key: "question.pin", Value: `0`}, + {ID: 120, Key: "question.unpin", Value: `0`}, + {ID: 121, Key: "question.show", Value: `0`}, + {ID: 122, Key: "question.hide", Value: `0`}, + {ID: 123, Key: "rank.question.pin", Value: `-1`}, + {ID: 124, Key: "rank.question.unpin", Value: `-1`}, + {ID: 125, Key: "rank.question.show", Value: `-1`}, + {ID: 126, Key: "rank.question.hide", Value: `-1`}, + {ID: 127, Key: "rank.answer.invite_someone_to_answer", Value: `1000`}, + } +) diff --git a/internal/migrations/migrations.go b/internal/migrations/migrations.go index c9747c11..ecfff1b7 100644 --- a/internal/migrations/migrations.go +++ b/internal/migrations/migrations.go @@ -15,14 +15,14 @@ const minDBVersion = 0 type Migration interface { Version() string Description() string - Migrate(*xorm.Engine) error + Migrate(ctx context.Context, x *xorm.Engine) error ShouldCleanCache() bool } type migration struct { version string description string - migrate func(*xorm.Engine) error + migrate func(ctx context.Context, x *xorm.Engine) error shouldCleanCache bool } @@ -37,8 +37,8 @@ func (m *migration) Description() string { } // Migrate executes the migration -func (m *migration) Migrate(x *xorm.Engine) error { - return m.migrate(x) +func (m *migration) Migrate(ctx context.Context, x *xorm.Engine) error { + return m.migrate(ctx, x) } // ShouldCleanCache should clean the cache @@ -47,12 +47,12 @@ func (m *migration) ShouldCleanCache() bool { } // NewMigration creates a new migration -func NewMigration(version, desc string, fn func(*xorm.Engine) error, shouldCleanCache bool) Migration { +func NewMigration(version, desc string, fn func(ctx context.Context, x *xorm.Engine) error, shouldCleanCache bool) Migration { return &migration{version: version, description: desc, migrate: fn, shouldCleanCache: shouldCleanCache} } // Use noopMigration when there is a migration that has been no-oped -var noopMigration = func(_ *xorm.Engine) error { return nil } +var noopMigration = func(_ context.Context, _ *xorm.Engine) error { return nil } var migrations = []Migration{ // 0->1 @@ -70,6 +70,11 @@ var migrations = []Migration{ NewMigration("v1.1.0-beta.1", "update user pin hide features", updateRolePinAndHideFeatures, true), NewMigration("v1.1.0-beta.2", "update question post time", updateQuestionPostTime, true), NewMigration("v1.1.0", "add gravatar base url", updateCount, true), + NewMigration("v1.1.1", "update the length of revision content", updateTheLengthOfRevisionContent, false), +} + +func GetMigrations() []Migration { + return migrations } // GetCurrentDBVersion returns the current db version @@ -99,12 +104,12 @@ func ExpectedVersion() int64 { } // Migrate database to current version -func Migrate(dbConf *data.Database, cacheConf *data.CacheConf, upgradeToSpecificVersion string) error { +func Migrate(debug bool, dbConf *data.Database, cacheConf *data.CacheConf, upgradeToSpecificVersion string) error { cache, cacheCleanup, err := data.NewCache(cacheConf) if err != nil { fmt.Println("new check failed:", err.Error()) } - engine, err := data.NewDB(false, dbConf) + engine, err := data.NewDB(debug, dbConf) if err != nil { fmt.Println("new database failed: ", err.Error()) return err @@ -130,7 +135,7 @@ func Migrate(dbConf *data.Database, cacheConf *data.CacheConf, upgradeToSpecific currentDBVersion, currentDBVersion+1, expectedVersion) migrationFunc := migrations[currentDBVersion] fmt.Printf("[migrate] try to migrate Answer version %s, description: %s\n", migrationFunc.Version(), migrationFunc.Description()) - if err := migrationFunc.Migrate(engine); err != nil { + if err := migrationFunc.Migrate(context.Background(), engine); err != nil { fmt.Printf("[migrate] migrate to db version %d failed: %s\n", currentDBVersion+1, err.Error()) return err } diff --git a/internal/migrations/v1.go b/internal/migrations/v1.go index e929d45c..de6ba987 100644 --- a/internal/migrations/v1.go +++ b/internal/migrations/v1.go @@ -1,14 +1,15 @@ package migrations import ( + "context" "xorm.io/xorm" ) -func addUserLanguage(x *xorm.Engine) error { +func addUserLanguage(ctx context.Context, 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)) + return x.Context(ctx).Sync(new(User)) } diff --git a/internal/migrations/v10.go b/internal/migrations/v10.go index 2d669b19..6d8eb240 100644 --- a/internal/migrations/v10.go +++ b/internal/migrations/v10.go @@ -1,6 +1,7 @@ package migrations import ( + "context" "encoding/json" "fmt" @@ -11,11 +12,11 @@ import ( "xorm.io/xorm" ) -func addLoginLimitations(x *xorm.Engine) error { +func addLoginLimitations(ctx context.Context, x *xorm.Engine) error { loginSiteInfo := &entity.SiteInfo{ Type: constant.SiteTypeLogin, } - exist, err := x.Get(loginSiteInfo) + exist, err := x.Context(ctx).Get(loginSiteInfo) if err != nil { return fmt.Errorf("get config failed: %w", err) } @@ -26,7 +27,7 @@ func addLoginLimitations(x *xorm.Engine) error { content.AllowEmailDomains = make([]string, 0) data, _ := json.Marshal(content) loginSiteInfo.Content = string(data) - _, err = x.ID(loginSiteInfo.ID).Cols("content").Update(loginSiteInfo) + _, err = x.Context(ctx).ID(loginSiteInfo.ID).Cols("content").Update(loginSiteInfo) if err != nil { return fmt.Errorf("update site info failed: %w", err) } @@ -35,7 +36,7 @@ func addLoginLimitations(x *xorm.Engine) error { interfaceSiteInfo := &entity.SiteInfo{ Type: constant.SiteTypeInterface, } - exist, err = x.Get(interfaceSiteInfo) + exist, err = x.Context(ctx).Get(interfaceSiteInfo) if err != nil { return fmt.Errorf("get config failed: %w", err) } @@ -52,7 +53,7 @@ func addLoginLimitations(x *xorm.Engine) error { } data, _ := json.Marshal(siteUsers) - exist, err = x.Get(&entity.SiteInfo{Type: constant.SiteTypeUsers}) + exist, err = x.Context(ctx).Get(&entity.SiteInfo{Type: constant.SiteTypeUsers}) if err != nil { return fmt.Errorf("get config failed: %w", err) } @@ -62,7 +63,7 @@ func addLoginLimitations(x *xorm.Engine) error { Content: string(data), Status: 1, } - _, err = x.InsertOne(usersSiteInfo) + _, err = x.Context(ctx).Insert(usersSiteInfo) if err != nil { return fmt.Errorf("insert site info failed: %w", err) } diff --git a/internal/migrations/v11.go b/internal/migrations/v11.go index 5d9b8725..409320be 100644 --- a/internal/migrations/v11.go +++ b/internal/migrations/v11.go @@ -1,6 +1,7 @@ package migrations import ( + "context" "fmt" "github.com/answerdev/answer/internal/entity" @@ -8,8 +9,7 @@ import ( "xorm.io/xorm" ) -func updateRolePinAndHideFeatures(x *xorm.Engine) error { - +func updateRolePinAndHideFeatures(ctx context.Context, x *xorm.Engine) error { defaultConfigTable := []*entity.Config{ {ID: 119, Key: "question.pin", Value: `0`}, {ID: 120, Key: "question.unpin", Value: `0`}, @@ -21,18 +21,18 @@ func updateRolePinAndHideFeatures(x *xorm.Engine) error { {ID: 126, Key: "rank.question.hide", Value: `-1`}, } for _, c := range defaultConfigTable { - exist, err := x.Get(&entity.Config{ID: c.ID}) + exist, err := x.Context(ctx).Get(&entity.Config{ID: c.ID}) if err != nil { return fmt.Errorf("get config failed: %w", err) } if exist { - if _, err = x.Update(c, &entity.Config{ID: c.ID}); err != nil { + if _, err = x.Context(ctx).Update(c, &entity.Config{ID: c.ID}); 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 { + if _, err = x.Context(ctx).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) } diff --git a/internal/migrations/v12.go b/internal/migrations/v12.go index 850146ed..6f547a15 100644 --- a/internal/migrations/v12.go +++ b/internal/migrations/v12.go @@ -1,6 +1,7 @@ package migrations import ( + "context" "fmt" "time" @@ -37,9 +38,9 @@ func (QuestionPostTime) TableName() string { return "question" } -func updateQuestionPostTime(x *xorm.Engine) error { +func updateQuestionPostTime(ctx context.Context, x *xorm.Engine) error { questionList := make([]QuestionPostTime, 0) - err := x.Find(&questionList, &entity.Question{}) + err := x.Context(ctx).Find(&questionList, &entity.Question{}) if err != nil { return fmt.Errorf("get questions failed: %w", err) } @@ -50,7 +51,7 @@ func updateQuestionPostTime(x *xorm.Engine) error { } else if !item.CreatedAt.IsZero() { item.PostUpdateTime = item.CreatedAt } - if _, err = x.Update(item, &QuestionPostTime{ID: item.ID}); err != nil { + if _, err = x.Context(ctx).Update(item, &QuestionPostTime{ID: item.ID}); err != nil { log.Errorf("update %+v config failed: %s", item, err) return fmt.Errorf("update question failed: %w", err) } diff --git a/internal/migrations/v13.go b/internal/migrations/v13.go index d467a4a4..abf7982f 100644 --- a/internal/migrations/v13.go +++ b/internal/migrations/v13.go @@ -1,6 +1,7 @@ package migrations import ( + "context" "encoding/json" "fmt" "time" @@ -13,8 +14,8 @@ import ( "xorm.io/xorm" ) -func updateCount(x *xorm.Engine) error { - fns := []func(*xorm.Engine) error{ +func updateCount(ctx context.Context, x *xorm.Engine) error { + fns := []func(ctx context.Context, x *xorm.Engine) error{ inviteAnswer, addPrivilegeForInviteSomeoneToAnswer, addGravatarBaseURL, @@ -25,18 +26,18 @@ func updateCount(x *xorm.Engine) error { inBoxData, } for _, fn := range fns { - if err := fn(x); err != nil { + if err := fn(ctx, x); err != nil { return err } } return nil } -func addGravatarBaseURL(x *xorm.Engine) error { +func addGravatarBaseURL(ctx context.Context, x *xorm.Engine) error { usersSiteInfo := &entity.SiteInfo{ Type: constant.SiteTypeUsers, } - exist, err := x.Get(usersSiteInfo) + exist, err := x.Context(ctx).Get(usersSiteInfo) if err != nil { return fmt.Errorf("get config failed: %w", err) } @@ -47,7 +48,7 @@ func addGravatarBaseURL(x *xorm.Engine) error { data, _ := json.Marshal(content) usersSiteInfo.Content = string(data) - _, err = x.ID(usersSiteInfo.ID).Cols("content").Update(usersSiteInfo) + _, err = x.Context(ctx).ID(usersSiteInfo.ID).Cols("content").Update(usersSiteInfo) if err != nil { return fmt.Errorf("update site info failed: %w", err) } @@ -55,20 +56,20 @@ func addGravatarBaseURL(x *xorm.Engine) error { return nil } -func addPrivilegeForInviteSomeoneToAnswer(x *xorm.Engine) error { +func addPrivilegeForInviteSomeoneToAnswer(ctx context.Context, x *xorm.Engine) error { // add rank for invite to answer powers := []*entity.Power{ {ID: 38, Name: "invite someone to answer", PowerType: permission.AnswerInviteSomeoneToAnswer, Description: "invite someone to answer"}, } for _, power := range powers { - exist, err := x.Get(&entity.Power{PowerType: power.PowerType}) + exist, err := x.Context(ctx).Get(&entity.Power{PowerType: power.PowerType}) if err != nil { return err } if exist { - _, err = x.ID(power.ID).Update(power) + _, err = x.Context(ctx).ID(power.ID).Update(power) } else { - _, err = x.Insert(power) + _, err = x.Context(ctx).Insert(power) } if err != nil { return err @@ -79,14 +80,14 @@ func addPrivilegeForInviteSomeoneToAnswer(x *xorm.Engine) error { {RoleID: 3, PowerType: permission.AnswerInviteSomeoneToAnswer}, } for _, rel := range rolePowerRels { - exist, err := x.Get(&entity.RolePowerRel{RoleID: rel.RoleID, PowerType: rel.PowerType}) + exist, err := x.Context(ctx).Get(&entity.RolePowerRel{RoleID: rel.RoleID, PowerType: rel.PowerType}) if err != nil { return err } if exist { continue } - _, err = x.Insert(rel) + _, err = x.Context(ctx).Insert(rel) if err != nil { return err } @@ -96,27 +97,27 @@ func addPrivilegeForInviteSomeoneToAnswer(x *xorm.Engine) error { {ID: 127, Key: "rank.answer.invite_someone_to_answer", Value: `1000`}, } for _, c := range defaultConfigTable { - exist, err := x.Get(&entity.Config{ID: c.ID}) + exist, err := x.Context(ctx).Get(&entity.Config{ID: c.ID}) if err != nil { return fmt.Errorf("get config failed: %w", err) } if exist { - if _, err = x.Update(c, &entity.Config{ID: c.ID}); err != nil { + if _, err = x.Context(ctx).Update(c, &entity.Config{ID: c.ID}); err != nil { 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 { + if _, err = x.Context(ctx).Insert(&entity.Config{ID: c.ID, Key: c.Key, Value: c.Value}); err != nil { return fmt.Errorf("add config failed: %w", err) } } return nil } -func updateQuestionCount(x *xorm.Engine) error { +func updateQuestionCount(ctx context.Context, x *xorm.Engine) error { //question answer count answers := make([]AnswerV13, 0) - err := x.Find(&answers, &AnswerV13{Status: entity.AnswerStatusAvailable}) + err := x.Context(ctx).Find(&answers, &AnswerV13{Status: entity.AnswerStatusAvailable}) if err != nil { return fmt.Errorf("get answers failed: %w", err) } @@ -130,7 +131,7 @@ func updateQuestionCount(x *xorm.Engine) error { } } questionList := make([]QuestionV13, 0) - err = x.Find(&questionList, &QuestionV13{}) + err = x.Context(ctx).Find(&questionList, &QuestionV13{}) if err != nil { return fmt.Errorf("get questions failed: %w", err) } @@ -138,7 +139,7 @@ func updateQuestionCount(x *xorm.Engine) error { _, ok := questionAnswerCount[item.ID] if ok { item.AnswerCount = questionAnswerCount[item.ID] - if _, err = x.Cols("answer_count").Update(item, &QuestionV13{ID: item.ID}); err != nil { + if _, err = x.Context(ctx).Cols("answer_count").Update(item, &QuestionV13{ID: item.ID}); err != nil { log.Errorf("update %+v config failed: %s", item, err) return fmt.Errorf("update question failed: %w", err) } @@ -149,9 +150,9 @@ func updateQuestionCount(x *xorm.Engine) error { } // updateTagCount update tag count -func updateTagCount(x *xorm.Engine) error { +func updateTagCount(ctx context.Context, x *xorm.Engine) error { tagRelList := make([]entity.TagRel, 0) - err := x.Find(&tagRelList, &entity.TagRel{}) + err := x.Context(ctx).Find(&tagRelList, &entity.TagRel{}) if err != nil { return fmt.Errorf("get tag rel failed: %w", err) } @@ -164,7 +165,7 @@ func updateTagCount(x *xorm.Engine) error { questionsHideMap[item.ObjectID] = false } questionList := make([]QuestionV13, 0) - err = x.In("id", questionIDs).In("question.status", []int{entity.QuestionStatusAvailable, entity.QuestionStatusClosed}).Find(&questionList, &QuestionV13{}) + err = x.Context(ctx).In("id", questionIDs).In("question.status", []int{entity.QuestionStatusAvailable, entity.QuestionStatusClosed}).Find(&questionList, &QuestionV13{}) if err != nil { return fmt.Errorf("get questions failed: %w", err) } @@ -180,7 +181,7 @@ func updateTagCount(x *xorm.Engine) error { for id, ok := range questionsHideMap { if ok { - if _, err = x.Cols("status").Update(&entity.TagRel{Status: entity.TagRelStatusHide}, &entity.TagRel{ObjectID: id}); err != nil { + if _, err = x.Context(ctx).Cols("status").Update(&entity.TagRel{Status: entity.TagRelStatusHide}, &entity.TagRel{ObjectID: id}); err != nil { log.Errorf("update %+v config failed: %s", id, err) } } @@ -188,7 +189,7 @@ func updateTagCount(x *xorm.Engine) error { for id, ok := range questionsAvailableMap { if !ok { - if _, err = x.Cols("status").Update(&entity.TagRel{Status: entity.TagRelStatusDeleted}, &entity.TagRel{ObjectID: id}); err != nil { + if _, err = x.Context(ctx).Cols("status").Update(&entity.TagRel{Status: entity.TagRelStatusDeleted}, &entity.TagRel{ObjectID: id}); err != nil { log.Errorf("update %+v config failed: %s", id, err) } } @@ -196,7 +197,7 @@ func updateTagCount(x *xorm.Engine) error { //select tag count newTagRelList := make([]entity.TagRel, 0) - err = x.Find(&newTagRelList, &entity.TagRel{Status: entity.TagRelStatusAvailable}) + err = x.Context(ctx).Find(&newTagRelList, &entity.TagRel{Status: entity.TagRelStatusAvailable}) if err != nil { return fmt.Errorf("get tag rel failed: %w", err) } @@ -210,7 +211,7 @@ func updateTagCount(x *xorm.Engine) error { } } TagList := make([]entity.Tag, 0) - err = x.Find(&TagList, &entity.Tag{}) + err = x.Context(ctx).Find(&TagList, &entity.Tag{}) if err != nil { return fmt.Errorf("get tag failed: %w", err) } @@ -218,13 +219,13 @@ func updateTagCount(x *xorm.Engine) error { _, ok := tagCountMap[tag.ID] if ok { tag.QuestionCount = tagCountMap[tag.ID] - if _, err = x.Update(tag, &entity.Tag{ID: tag.ID}); err != nil { + if _, err = x.Context(ctx).Update(tag, &entity.Tag{ID: tag.ID}); err != nil { log.Errorf("update %+v tag failed: %s", tag.ID, err) return fmt.Errorf("update tag failed: %w", err) } } else { tag.QuestionCount = 0 - if _, err = x.Cols("question_count").Update(tag, &entity.Tag{ID: tag.ID}); err != nil { + if _, err = x.Context(ctx).Cols("question_count").Update(tag, &entity.Tag{ID: tag.ID}); err != nil { log.Errorf("update %+v tag failed: %s", tag.ID, err) return fmt.Errorf("update tag failed: %w", err) } @@ -234,9 +235,9 @@ func updateTagCount(x *xorm.Engine) error { } // updateUserQuestionCount update user question count -func updateUserQuestionCount(x *xorm.Engine) error { +func updateUserQuestionCount(ctx context.Context, x *xorm.Engine) error { questionList := make([]QuestionV13, 0) - err := x.In("status", []int{entity.QuestionStatusAvailable, entity.QuestionStatusClosed}).Find(&questionList, &QuestionV13{}) + err := x.Context(ctx).In("status", []int{entity.QuestionStatusAvailable, entity.QuestionStatusClosed}).Find(&questionList, &QuestionV13{}) if err != nil { return fmt.Errorf("get question failed: %w", err) } @@ -250,7 +251,7 @@ func updateUserQuestionCount(x *xorm.Engine) error { } } userList := make([]entity.User, 0) - err = x.Find(&userList, &entity.User{}) + err = x.Context(ctx).Find(&userList, &entity.User{}) if err != nil { return fmt.Errorf("get user failed: %w", err) } @@ -258,13 +259,13 @@ func updateUserQuestionCount(x *xorm.Engine) error { _, ok := userQuestionCountMap[user.ID] if ok { user.QuestionCount = userQuestionCountMap[user.ID] - if _, err = x.Cols("question_count").Update(user, &entity.User{ID: user.ID}); err != nil { + if _, err = x.Context(ctx).Cols("question_count").Update(user, &entity.User{ID: user.ID}); err != nil { log.Errorf("update %+v user failed: %s", user.ID, err) return fmt.Errorf("update user failed: %w", err) } } else { user.QuestionCount = 0 - if _, err = x.Cols("question_count").Update(user, &entity.User{ID: user.ID}); err != nil { + if _, err = x.Context(ctx).Cols("question_count").Update(user, &entity.User{ID: user.ID}); err != nil { log.Errorf("update %+v user failed: %s", user.ID, err) return fmt.Errorf("update user failed: %w", err) } @@ -286,9 +287,9 @@ func (AnswerV13) TableName() string { } // updateUserAnswerCount update user answer count -func updateUserAnswerCount(x *xorm.Engine) error { +func updateUserAnswerCount(ctx context.Context, x *xorm.Engine) error { answers := make([]AnswerV13, 0) - err := x.Find(&answers, &AnswerV13{Status: entity.AnswerStatusAvailable}) + err := x.Context(ctx).Find(&answers, &AnswerV13{Status: entity.AnswerStatusAvailable}) if err != nil { return fmt.Errorf("get answers failed: %w", err) } @@ -302,7 +303,7 @@ func updateUserAnswerCount(x *xorm.Engine) error { } } userList := make([]entity.User, 0) - err = x.Find(&userList, &entity.User{}) + err = x.Context(ctx).Find(&userList, &entity.User{}) if err != nil { return fmt.Errorf("get user failed: %w", err) } @@ -310,13 +311,13 @@ func updateUserAnswerCount(x *xorm.Engine) error { _, ok := userAnswerCount[user.ID] if ok { user.AnswerCount = userAnswerCount[user.ID] - if _, err = x.Cols("answer_count").Update(user, &entity.User{ID: user.ID}); err != nil { + if _, err = x.Context(ctx).Cols("answer_count").Update(user, &entity.User{ID: user.ID}); err != nil { log.Errorf("update %+v user failed: %s", user.ID, err) return fmt.Errorf("update user failed: %w", err) } } else { user.AnswerCount = 0 - if _, err = x.Cols("answer_count").Update(user, &entity.User{ID: user.ID}); err != nil { + if _, err = x.Context(ctx).Cols("answer_count").Update(user, &entity.User{ID: user.ID}); err != nil { log.Errorf("update %+v user failed: %s", user.ID, err) return fmt.Errorf("update user failed: %w", err) } @@ -354,8 +355,8 @@ func (QuestionV13) TableName() string { return "question" } -func inviteAnswer(x *xorm.Engine) error { - err := x.Sync(new(QuestionV13)) +func inviteAnswer(ctx context.Context, x *xorm.Engine) error { + err := x.Context(ctx).Sync(new(QuestionV13)) if err != nil { return err } @@ -363,7 +364,7 @@ func inviteAnswer(x *xorm.Engine) error { } // inBoxData Classify messages -func inBoxData(x *xorm.Engine) error { +func inBoxData(ctx context.Context, x *xorm.Engine) error { type Notification struct { ID string `xorm:"not null pk autoincr BIGINT(20) id"` CreatedAt time.Time `xorm:"created TIMESTAMP created_at"` @@ -376,12 +377,12 @@ func inBoxData(x *xorm.Engine) error { IsRead int `xorm:"not null default 1 INT(11) is_read"` Status int `xorm:"not null default 1 INT(11) status"` } - err := x.Sync(new(Notification)) + err := x.Context(ctx).Sync(new(Notification)) if err != nil { return err } msglist := make([]entity.Notification, 0) - err = x.Find(&msglist, &entity.Notification{}) + err = x.Context(ctx).Find(&msglist, &entity.Notification{}) if err != nil { return fmt.Errorf("get Notification failed: %w", err) } @@ -394,7 +395,7 @@ func inBoxData(x *xorm.Engine) error { _, ok := constant.NotificationMsgTypeMapping[Content.NotificationAction] if ok { v.MsgType = constant.NotificationMsgTypeMapping[Content.NotificationAction] - if _, err = x.Update(v, &entity.Notification{ID: v.ID}); err != nil { + if _, err = x.Context(ctx).Update(v, &entity.Notification{ID: v.ID}); err != nil { log.Errorf("update %+v Notification failed: %s", v.ID, err) } } diff --git a/internal/migrations/v14.go b/internal/migrations/v14.go new file mode 100644 index 00000000..e9f960b4 --- /dev/null +++ b/internal/migrations/v14.go @@ -0,0 +1,35 @@ +package migrations + +import ( + "context" + "time" + "xorm.io/xorm/schemas" + + "xorm.io/xorm" +) + +func updateTheLengthOfRevisionContent(ctx context.Context, x *xorm.Engine) (err error) { + sess := x.Context(ctx) + if x.Dialect().URI().DBType == schemas.MYSQL { + _, err = sess.Exec("ALTER TABLE `revision` CHANGE `content` `content` MEDIUMTEXT NOT NULL;") + } + return err +} + +type RevisionV14 struct { + ID string `xorm:"not null pk autoincr BIGINT(20) 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"` + ObjectType int `xorm:"not null default 0 INT(11) object_type"` + ObjectID string `xorm:"not null default 0 BIGINT(20) INDEX object_id"` + Title string `xorm:"not null default '' VARCHAR(255) title"` + Content string `xorm:"not null MEDIUMTEXT content"` + Log string `xorm:"VARCHAR(255) log"` + Status int `xorm:"not null default 1 INT(11) status"` + ReviewUserID int64 `xorm:"not null default 0 BIGINT(20) review_user_id"` +} + +func (RevisionV14) TableName() string { + return "revision" +} diff --git a/internal/migrations/v2.go b/internal/migrations/v2.go index 368124ef..92b1b3c0 100644 --- a/internal/migrations/v2.go +++ b/internal/migrations/v2.go @@ -1,15 +1,16 @@ package migrations import ( + "context" "xorm.io/xorm" ) -func addTagRecommendedAndReserved(x *xorm.Engine) error { +func addTagRecommendedAndReserved(ctx context.Context, x *xorm.Engine) error { type Tag struct { 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)) + return x.Context(ctx).Sync(new(Tag)) } diff --git a/internal/migrations/v3.go b/internal/migrations/v3.go index 2d9b28eb..a0833645 100644 --- a/internal/migrations/v3.go +++ b/internal/migrations/v3.go @@ -1,6 +1,7 @@ package migrations import ( + "context" "fmt" "time" @@ -10,28 +11,28 @@ import ( "xorm.io/xorm/schemas" ) -func addActivityTimeline(x *xorm.Engine) (err error) { +func addActivityTimeline(ctx context.Context, x *xorm.Engine) (err error) { switch x.Dialect().URI().DBType { case schemas.MYSQL: - _, err = x.Exec("ALTER TABLE `answer` CHANGE `updated_at` `updated_at` TIMESTAMP NULL DEFAULT NULL") + _, err = x.Context(ctx).Exec("ALTER TABLE `answer` CHANGE `updated_at` `updated_at` TIMESTAMP NULL DEFAULT NULL") if err != nil { return err } - _, err = x.Exec("ALTER TABLE `question` CHANGE `updated_at` `updated_at` TIMESTAMP NULL DEFAULT NULL") + _, err = x.Context(ctx).Exec("ALTER TABLE `question` CHANGE `updated_at` `updated_at` TIMESTAMP NULL DEFAULT NULL") if err != nil { return err } case schemas.POSTGRES: - _, err = x.Exec(`ALTER TABLE "answer" ALTER COLUMN "updated_at" DROP NOT NULL, ALTER COLUMN "updated_at" SET DEFAULT NULL`) + _, err = x.Context(ctx).Exec(`ALTER TABLE "answer" ALTER COLUMN "updated_at" DROP NOT NULL, ALTER COLUMN "updated_at" SET DEFAULT NULL`) if err != nil { return err } - _, err = x.Exec(`ALTER TABLE "question" ALTER COLUMN "updated_at" DROP NOT NULL, ALTER COLUMN "updated_at" SET DEFAULT NULL`) + _, err = x.Context(ctx).Exec(`ALTER TABLE "question" ALTER COLUMN "updated_at" DROP NOT NULL, ALTER COLUMN "updated_at" SET DEFAULT NULL`) if err != nil { return err } case schemas.SQLITE: - _, err = x.Exec(`DROP INDEX "IDX_answer_user_id"; + _, err = x.Context(ctx).Exec(`DROP INDEX "IDX_answer_user_id"; ALTER TABLE "answer" RENAME TO "_answer_old_v3"; @@ -98,7 +99,7 @@ ON "question" ( 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 { + if err := x.Context(ctx).Sync(new(Config)); err != nil { return fmt.Errorf("sync config table failed: %w", err) } defaultConfigTable := []*entity.Config{ @@ -117,7 +118,7 @@ ON "question" ( {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: 51, Key: "rank.tag.add", Value: `1500`}, {ID: 52, Key: "rank.tag.edit", Value: `100`}, {ID: 53, Key: "rank.tag.delete", Value: `-1`}, {ID: 54, Key: "rank.tag.synonym", Value: `20000`}, @@ -155,18 +156,18 @@ ON "question" ( {ID: 114, Key: "rank.tag.audit", Value: `20000`}, } for _, c := range defaultConfigTable { - exist, err := x.Get(&entity.Config{ID: c.ID, Key: c.Key}) + exist, err := x.Context(ctx).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 { + if _, err = x.Context(ctx).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 { + if _, err = x.Context(ctx).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) } @@ -205,7 +206,7 @@ ON "question" ( LastEditUserID string `xorm:"not null default 0 BIGINT(20) last_edit_user_id"` } - err = x.Sync(new(Activity), new(Revision), new(Tag), new(Question), new(Answer)) + err = x.Context(ctx).Sync(new(Activity), new(Revision), new(Tag), new(Question), new(Answer)) if err != nil { return fmt.Errorf("sync table failed %w", err) } diff --git a/internal/migrations/v4.go b/internal/migrations/v4.go index 9d32b13f..b0aca5e5 100644 --- a/internal/migrations/v4.go +++ b/internal/migrations/v4.go @@ -1,6 +1,7 @@ package migrations import ( + "context" "fmt" "github.com/answerdev/answer/internal/entity" @@ -9,8 +10,8 @@ import ( "xorm.io/xorm" ) -func addRoleFeatures(x *xorm.Engine) error { - err := x.Sync(new(entity.Role), new(entity.RolePowerRel), new(entity.Power), new(entity.UserRoleRel)) +func addRoleFeatures(ctx context.Context, x *xorm.Engine) error { + err := x.Context(ctx).Sync(new(entity.Role), new(entity.RolePowerRel), new(entity.Power), new(entity.UserRoleRel)) if err != nil { return err } @@ -23,14 +24,14 @@ func addRoleFeatures(x *xorm.Engine) error { // insert default roles for _, role := range roles { - exist, err := x.Get(&entity.Role{ID: role.ID, Name: role.Name}) + exist, err := x.Context(ctx).Get(&entity.Role{ID: role.ID, Name: role.Name}) if err != nil { return err } if exist { continue } - _, err = x.Insert(role) + _, err = x.Context(ctx).Insert(role) if err != nil { return err } @@ -73,14 +74,14 @@ func addRoleFeatures(x *xorm.Engine) error { } // insert default powers for _, power := range powers { - exist, err := x.Get(&entity.Power{ID: power.ID}) + exist, err := x.Context(ctx).Get(&entity.Power{ID: power.ID}) if err != nil { return err } if exist { - _, err = x.ID(power.ID).Update(power) + _, err = x.Context(ctx).ID(power.ID).Update(power) } else { - _, err = x.Insert(power) + _, err = x.Context(ctx).Insert(power) } if err != nil { return err @@ -160,14 +161,14 @@ func addRoleFeatures(x *xorm.Engine) error { // insert default powers for _, rel := range rolePowerRels { - exist, err := x.Get(&entity.RolePowerRel{RoleID: rel.RoleID, PowerType: rel.PowerType}) + exist, err := x.Context(ctx).Get(&entity.RolePowerRel{RoleID: rel.RoleID, PowerType: rel.PowerType}) if err != nil { return err } if exist { continue } - _, err = x.Insert(rel) + _, err = x.Context(ctx).Insert(rel) if err != nil { return err } @@ -178,12 +179,12 @@ func addRoleFeatures(x *xorm.Engine) error { RoleID: 2, } - exist, err := x.Get(adminUserRoleRel) + exist, err := x.Context(ctx).Get(adminUserRoleRel) if err != nil { return err } if !exist { - _, err = x.Insert(adminUserRoleRel) + _, err = x.Context(ctx).Insert(adminUserRoleRel) if err != nil { return err } @@ -195,18 +196,18 @@ func addRoleFeatures(x *xorm.Engine) error { {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}) + exist, err := x.Context(ctx).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 { + if _, err = x.Context(ctx).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 { + if _, err = x.Context(ctx).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) } diff --git a/internal/migrations/v5.go b/internal/migrations/v5.go index c4e3a4cc..8357837d 100644 --- a/internal/migrations/v5.go +++ b/internal/migrations/v5.go @@ -1,6 +1,7 @@ package migrations import ( + "context" "encoding/json" "fmt" @@ -8,7 +9,7 @@ import ( "xorm.io/xorm" ) -func addThemeAndPrivateMode(x *xorm.Engine) error { +func addThemeAndPrivateMode(ctx context.Context, x *xorm.Engine) error { loginConfig := map[string]bool{ "allow_new_registrations": true, "login_required": false, @@ -19,12 +20,12 @@ func addThemeAndPrivateMode(x *xorm.Engine) error { Content: string(loginConfigDataBytes), Status: 1, } - exist, err := x.Get(&entity.SiteInfo{Type: siteInfo.Type}) + exist, err := x.Context(ctx).Get(&entity.SiteInfo{Type: siteInfo.Type}) if err != nil { return fmt.Errorf("get config failed: %w", err) } if !exist { - _, err = x.InsertOne(siteInfo) + _, err = x.Context(ctx).Insert(siteInfo) if err != nil { return fmt.Errorf("insert site info failed: %w", err) } @@ -36,12 +37,12 @@ func addThemeAndPrivateMode(x *xorm.Engine) error { Content: themeConfig, Status: 1, } - exist, err = x.Get(&entity.SiteInfo{Type: themeSiteInfo.Type}) + exist, err = x.Context(ctx).Get(&entity.SiteInfo{Type: themeSiteInfo.Type}) if err != nil { return fmt.Errorf("get config failed: %w", err) } if !exist { - _, err = x.InsertOne(themeSiteInfo) + _, err = x.Context(ctx).Insert(themeSiteInfo) } return err } diff --git a/internal/migrations/v6.go b/internal/migrations/v6.go index 0f87b367..32747c64 100644 --- a/internal/migrations/v6.go +++ b/internal/migrations/v6.go @@ -1,6 +1,7 @@ package migrations import ( + "context" "encoding/json" "fmt" @@ -8,15 +9,15 @@ import ( "xorm.io/xorm" ) -func addNewAnswerNotification(x *xorm.Engine) error { +func addNewAnswerNotification(ctx context.Context, x *xorm.Engine) error { cond := &entity.Config{Key: "email.config"} - exists, err := x.Get(cond) + exists, err := x.Context(ctx).Get(cond) if err != nil { return fmt.Errorf("get email config failed: %w", err) } if !exists { // This should be impossible except that the config was deleted manually by user. - _, err = x.InsertOne(&entity.Config{ + _, err = x.Context(ctx).Insert(&entity.Config{ 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.","new_answer_title":"[{{.SiteName}}] {{.DisplayName}} answered your question","new_answer_body":"{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.AnswerSummary}}

\nView it on {{.SiteName}}

\n\nYou are receiving this because you authored the thread. Unsubscribe","new_comment_title":"[{{.SiteName}}] {{.DisplayName}} commented on your post","new_comment_body":"{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.CommentSummary}}

\nView it on {{.SiteName}}

\n\nYou are receiving this because you authored the thread. Unsubscribe"}`, }) @@ -33,7 +34,7 @@ func addNewAnswerNotification(x *xorm.Engine) error { m["new_comment_body"] = "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.CommentSummary}}

\nView it on {{.SiteName}}

\n\nYou are receiving this because you authored the thread. Unsubscribe" val, _ := json.Marshal(m) - _, err = x.ID(cond.ID).Update(&entity.Config{Value: string(val)}) + _, err = x.Context(ctx).ID(cond.ID).Update(&entity.Config{Value: string(val)}) if err != nil { return fmt.Errorf("update email config failed: %v", err) } diff --git a/internal/migrations/v7.go b/internal/migrations/v7.go index 1770a80d..23f62024 100644 --- a/internal/migrations/v7.go +++ b/internal/migrations/v7.go @@ -1,6 +1,7 @@ package migrations import ( + "context" "fmt" "github.com/answerdev/answer/internal/entity" @@ -8,23 +9,23 @@ import ( "xorm.io/xorm" ) -func addPlugin(x *xorm.Engine) error { +func addPlugin(ctx context.Context, x *xorm.Engine) error { defaultConfigTable := []*entity.Config{ {ID: 118, Key: "plugin.status", Value: `{}`}, } for _, c := range defaultConfigTable { - exist, err := x.Get(&entity.Config{ID: c.ID, Key: c.Key}) + exist, err := x.Context(ctx).Get(&entity.Config{ID: c.ID, Key: c.Key}) if err != nil { return fmt.Errorf("get config failed: %w", err) } if exist { continue } - if _, err = x.Insert(&entity.Config{ID: c.ID, Key: c.Key, Value: c.Value}); err != nil { + if _, err = x.Context(ctx).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 x.Sync(new(entity.PluginConfig), new(entity.UserExternalLogin)) + return x.Context(ctx).Sync(new(entity.PluginConfig), new(entity.UserExternalLogin)) } diff --git a/internal/migrations/v8.go b/internal/migrations/v8.go index 0b3674c3..c4db5ba9 100644 --- a/internal/migrations/v8.go +++ b/internal/migrations/v8.go @@ -1,6 +1,7 @@ package migrations import ( + "context" "fmt" "time" @@ -10,8 +11,7 @@ import ( "xorm.io/xorm" ) -func addRolePinAndHideFeatures(x *xorm.Engine) error { - +func addRolePinAndHideFeatures(ctx context.Context, x *xorm.Engine) error { powers := []*entity.Power{ {ID: 34, Name: "question pin", PowerType: permission.QuestionPin, Description: "top the question"}, {ID: 35, Name: "question hide", PowerType: permission.QuestionHide, Description: "hide the question"}, @@ -20,14 +20,14 @@ func addRolePinAndHideFeatures(x *xorm.Engine) error { } // insert default powers for _, power := range powers { - exist, err := x.Get(&entity.Power{ID: power.ID}) + exist, err := x.Context(ctx).Get(&entity.Power{ID: power.ID}) if err != nil { return err } if exist { - _, err = x.ID(power.ID).Update(power) + _, err = x.Context(ctx).ID(power.ID).Update(power) } else { - _, err = x.Insert(power) + _, err = x.Context(ctx).Insert(power) } if err != nil { return err @@ -49,14 +49,14 @@ func addRolePinAndHideFeatures(x *xorm.Engine) error { // insert default powers for _, rel := range rolePowerRels { - exist, err := x.Get(&entity.RolePowerRel{RoleID: rel.RoleID, PowerType: rel.PowerType}) + exist, err := x.Context(ctx).Get(&entity.RolePowerRel{RoleID: rel.RoleID, PowerType: rel.PowerType}) if err != nil { return err } if exist { continue } - _, err = x.Insert(rel) + _, err = x.Context(ctx).Insert(rel) if err != nil { return err } @@ -73,18 +73,18 @@ func addRolePinAndHideFeatures(x *xorm.Engine) error { {ID: 126, Key: "rank.question.hide", Value: `-1`}, } for _, c := range defaultConfigTable { - exist, err := x.Get(&entity.Config{ID: c.ID}) + exist, err := x.Context(ctx).Get(&entity.Config{ID: c.ID}) if err != nil { return fmt.Errorf("get config failed: %w", err) } if exist { - if _, err = x.Update(c, &entity.Config{ID: c.ID}); err != nil { + if _, err = x.Context(ctx).Update(c, &entity.Config{ID: c.ID}); 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 { + if _, err = x.Context(ctx).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) } @@ -113,7 +113,7 @@ func addRolePinAndHideFeatures(x *xorm.Engine) error { PostUpdateTime time.Time `xorm:"post_update_time TIMESTAMP"` RevisionID string `xorm:"not null default 0 BIGINT(20) revision_id"` } - err := x.Sync(new(Question)) + err := x.Context(ctx).Sync(new(Question)) if err != nil { return err } diff --git a/internal/migrations/v9.go b/internal/migrations/v9.go index fadd1bc6..c731c227 100644 --- a/internal/migrations/v9.go +++ b/internal/migrations/v9.go @@ -1,6 +1,7 @@ package migrations import ( + "context" "fmt" "github.com/answerdev/answer/internal/entity" @@ -8,9 +9,9 @@ import ( "xorm.io/xorm" ) -func updateAcceptAnswerRank(x *xorm.Engine) error { +func updateAcceptAnswerRank(ctx context.Context, x *xorm.Engine) error { c := &entity.Config{ID: 44, Key: "rank.answer.accept", Value: `-1`} - if _, err := x.Update(c, &entity.Config{ID: 44, Key: "rank.answer.accept"}); err != nil { + if _, err := x.Context(ctx).Update(c, &entity.Config{ID: 44, Key: "rank.answer.accept"}); err != nil { log.Errorf("update %+v config failed: %s", c, err) return fmt.Errorf("update config failed: %w", err) } diff --git a/internal/repo/activity/answer_repo.go b/internal/repo/activity/answer_repo.go index 6ed8bad4..fa4aacd0 100644 --- a/internal/repo/activity/answer_repo.go +++ b/internal/repo/activity/answer_repo.go @@ -2,7 +2,10 @@ package activity import ( "context" + "fmt" + "github.com/segmentfault/pacman/log" "time" + "xorm.io/builder" "github.com/answerdev/answer/internal/base/constant" "github.com/answerdev/answer/internal/base/data" @@ -15,343 +18,344 @@ import ( "github.com/answerdev/answer/internal/service/rank" "github.com/answerdev/answer/pkg/converter" "github.com/segmentfault/pacman/errors" - "github.com/segmentfault/pacman/log" "xorm.io/xorm" ) // AnswerActivityRepo answer accepted type AnswerActivityRepo struct { - data *data.Data - activityRepo activity_common.ActivityRepo - userRankRepo rank.UserRankRepo + data *data.Data + activityRepo activity_common.ActivityRepo + userRankRepo rank.UserRankRepo + notificationQueueService notice_queue.NotificationQueueService } -const ( - acceptAction = "accept" - acceptedAction = "accepted" -) - -var ( - acceptActionList = []string{acceptAction, acceptedAction} -) - // NewAnswerActivityRepo new repository func NewAnswerActivityRepo( data *data.Data, activityRepo activity_common.ActivityRepo, userRankRepo rank.UserRankRepo, + notificationQueueService notice_queue.NotificationQueueService, ) activity.AnswerActivityRepo { return &AnswerActivityRepo{ - data: data, - activityRepo: activityRepo, - userRankRepo: userRankRepo, + data: data, + activityRepo: activityRepo, + userRankRepo: userRankRepo, + notificationQueueService: notificationQueueService, } } -// NewQuestionActivityRepo new repository -func NewQuestionActivityRepo( - data *data.Data, - activityRepo activity_common.ActivityRepo, - userRankRepo rank.UserRankRepo, -) activity.QuestionActivityRepo { - return &AnswerActivityRepo{ - data: data, - activityRepo: activityRepo, - userRankRepo: userRankRepo, - } -} - -func (ar *AnswerActivityRepo) DeleteQuestion(ctx context.Context, questionID string) (err error) { - questionInfo := &entity.Question{} - exist, err := ar.data.DB.Context(ctx).Where("id = ?", questionID).Get(questionInfo) +func (ar *AnswerActivityRepo) SaveAcceptAnswerActivity(ctx context.Context, op *schema.AcceptAnswerOperationInfo) ( + err error) { + // pre check + noNeedToDo, err := ar.activityPreCheck(ctx, op) if err != nil { - return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + return err } - if !exist { + if noNeedToDo { return nil } - // get all this object activity - activityList := make([]*entity.Activity, 0) - session := ar.data.DB.Context(ctx).Where("has_rank = 1") - session.Where("cancelled = ?", entity.ActivityAvailable) - err = session.Find(&activityList, &entity.Activity{ObjectID: questionID}) - if err != nil { - return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() - } - if len(activityList) == 0 { - return nil - } - - log.Infof("questionInfo %s deleted will rollback activity %d", questionID, len(activityList)) - + ar.data.DB.ShowSQL(true) + // save activity _, err = ar.data.DB.Transaction(func(session *xorm.Session) (result any, err error) { session = session.Context(ctx) - for _, act := range activityList { - log.Infof("user %s rollback rank %d", act.UserID, -act.Rank) - _, e := ar.userRankRepo.TriggerUserRank( - ctx, session, act.UserID, -act.Rank, act.ActivityType) - if e != nil { - return nil, errors.InternalServer(reason.DatabaseError).WithError(e).WithStack() - } - if _, e := session.Where("id = ?", act.ID).Cols("cancelled", "cancelled_at"). - Update(&entity.Activity{Cancelled: entity.ActivityCancelled, CancelledAt: time.Now()}); e != nil { - return nil, errors.InternalServer(reason.DatabaseError).WithError(e).WithStack() - } + userInfoMapping, err := ar.acquireUserInfo(session, op.GetUserIDs()) + if err != nil { + return nil, err + } + + err = ar.saveActivitiesAvailable(session, op) + if err != nil { + return nil, err + } + + err = ar.changeUserRank(ctx, session, op, userInfoMapping) + if err != nil { + return nil, err } return nil, nil }) if err != nil { - return err + return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } - // get all answers - answerList := make([]*entity.Answer, 0) - err = ar.data.DB.Context(ctx).Find(&answerList, &entity.Answer{QuestionID: questionID}) + // notification + ar.sendAcceptAnswerNotification(ctx, op) + return nil +} + +func (ar *AnswerActivityRepo) SaveCancelAcceptAnswerActivity(ctx context.Context, op *schema.AcceptAnswerOperationInfo) ( + err error) { + // pre check + activities, err := ar.getExistActivity(ctx, op) + if err != nil { + return err + } + var userIDs []string + for _, act := range activities { + if act.Cancelled == entity.ActivityCancelled { + continue + } + userIDs = append(userIDs, act.UserID) + } + if len(userIDs) == 0 { + return nil + } + + // save activity + _, err = ar.data.DB.Transaction(func(session *xorm.Session) (result any, err error) { + session = session.Context(ctx) + + userInfoMapping, err := ar.acquireUserInfo(session, userIDs) + if err != nil { + return nil, err + } + + err = ar.cancelActivities(session, activities) + if err != nil { + return nil, err + } + + err = ar.rollbackUserRank(ctx, session, activities, userInfoMapping) + if err != nil { + return nil, err + } + return nil, nil + }) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } - for _, answerInfo := range answerList { - err = ar.DeleteAnswer(ctx, answerInfo.ID) + + // notification + ar.sendCancelAcceptAnswerNotification(ctx, op) + return nil +} + +func (ar *AnswerActivityRepo) activityPreCheck(ctx context.Context, op *schema.AcceptAnswerOperationInfo) ( + noNeedToDo bool, err error) { + activities, err := ar.getExistActivity(ctx, op) + if err != nil { + return false, err + } + done := 0 + for _, act := range activities { + if act.Cancelled == entity.ActivityAvailable { + done++ + } + } + return done == len(op.Activities), nil +} + +func (ar *AnswerActivityRepo) acquireUserInfo(session *xorm.Session, userIDs []string) (map[string]*entity.User, error) { + us := make([]*entity.User, 0) + err := session.In("id", userIDs).ForUpdate().Find(&us) + if err != nil { + log.Error(err) + return nil, err + } + + users := make(map[string]*entity.User, 0) + for _, u := range us { + users[u.ID] = u + } + return users, nil +} + +// saveActivitiesAvailable save activities +// If activity not exist it will be created or else will be updated +// If this activity is already exist, set activity rank to 0 +// So after this function, the activity rank will be correct for update user rank +func (ar *AnswerActivityRepo) saveActivitiesAvailable(session *xorm.Session, op *schema.AcceptAnswerOperationInfo) ( + err error) { + for _, act := range op.Activities { + existsActivity := &entity.Activity{} + exist, err := session. + Where(builder.Eq{"object_id": op.AnswerObjectID}). + And(builder.Eq{"user_id": act.ActivityUserID}). + And(builder.Eq{"trigger_user_id": act.TriggerUserID}). + And(builder.Eq{"activity_type": act.ActivityType}). + Get(existsActivity) + if err != nil { + return err + } + if exist && existsActivity.Cancelled == entity.ActivityAvailable { + act.Rank = 0 + continue + } + if exist { + bean := &entity.Activity{ + Cancelled: entity.ActivityAvailable, + Rank: act.Rank, + HasRank: act.HasRank(), + } + session.Where("id = ?", existsActivity.ID) + if _, err = session.Cols("`cancelled`", "`rank`", "`has_rank`").Update(bean); err != nil { + return err + } + } else { + insertActivity := entity.Activity{ + ObjectID: op.AnswerObjectID, + OriginalObjectID: act.OriginalObjectID, + UserID: act.ActivityUserID, + TriggerUserID: converter.StringToInt64(act.TriggerUserID), + ActivityType: act.ActivityType, + Rank: act.Rank, + HasRank: act.HasRank(), + Cancelled: entity.ActivityAvailable, + } + _, err = session.Insert(&insertActivity) + if err != nil { + return err + } + } + } + return nil +} + +// cancelActivities cancel activities +// If this activity is already cancelled, set activity rank to 0 +// So after this function, the activity rank will be correct for update user rank +func (ar *AnswerActivityRepo) cancelActivities(session *xorm.Session, activities []*entity.Activity) (err error) { + for _, act := range activities { + t := &entity.Activity{} + exist, err := session.ID(act.ID).Get(t) if err != nil { log.Error(err) + return err + } + if !exist { + log.Error(fmt.Errorf("%s activity not exist", act.ID)) + return fmt.Errorf("%s activity not exist", act.ID) + } + // If this activity is already cancelled, set activity rank to 0 + if t.Cancelled == entity.ActivityCancelled { + act.Rank = 0 + } + if _, err = session.ID(act.ID).Cols("cancelled", "cancelled_at"). + Update(&entity.Activity{ + Cancelled: entity.ActivityCancelled, + CancelledAt: time.Now(), + }); err != nil { + log.Error(err) + return err } } - return + return nil } -// AcceptAnswer accept other answer -func (ar *AnswerActivityRepo) AcceptAnswer(ctx context.Context, - answerObjID, questionObjID, questionUserID, answerUserID string, isSelf bool, -) (err error) { - addActivityList := make([]*entity.Activity, 0) - for _, action := range acceptActionList { - // get accept answer need add rank amount - activityType, deltaRank, hasRank, e := ar.activityRepo.GetActivityTypeByObjID(ctx, answerObjID, action) - if e != nil { - return errors.InternalServer(reason.DatabaseError).WithError(e).WithStack() +func (ar *AnswerActivityRepo) changeUserRank(ctx context.Context, session *xorm.Session, + op *schema.AcceptAnswerOperationInfo, + userInfoMapping map[string]*entity.User) (err error) { + for _, act := range op.Activities { + if act.Rank == 0 { + continue } - addActivity := &entity.Activity{ - ObjectID: answerObjID, - OriginalObjectID: questionObjID, - ActivityType: activityType, - Rank: deltaRank, - HasRank: hasRank, + user := userInfoMapping[act.ActivityUserID] + if user == nil { + continue } - if action == acceptAction { - addActivity.UserID = questionUserID - addActivity.TriggerUserID = converter.StringToInt64(answerUserID) - addActivity.OriginalObjectID = questionObjID // if activity is 'accept' means this question is accept the answer. - } else { - addActivity.UserID = answerUserID - addActivity.TriggerUserID = converter.StringToInt64(answerUserID) - addActivity.OriginalObjectID = answerObjID // if activity is 'accepted' means this answer was accepted. + if err = ar.userRankRepo.ChangeUserRank(ctx, session, + act.ActivityUserID, user.Rank, act.Rank); err != nil { + log.Error(err) + return err } - if isSelf { - addActivity.Rank = 0 - addActivity.HasRank = 0 - } - addActivityList = append(addActivityList, addActivity) } + return nil +} - _, err = ar.data.DB.Transaction(func(session *xorm.Session) (result any, err error) { - session = session.Context(ctx) - for _, addActivity := range addActivityList { - existsActivity, exists, e := ar.activityRepo.GetActivity( - ctx, session, answerObjID, addActivity.UserID, addActivity.ActivityType) - if e != nil { - return nil, errors.InternalServer(reason.DatabaseError).WithError(e).WithStack() - } - if exists && existsActivity.Cancelled == entity.ActivityAvailable { - continue - } - - // trigger user rank and send notification - if addActivity.Rank != 0 { - reachStandard, e := ar.userRankRepo.TriggerUserRank( - ctx, session, addActivity.UserID, addActivity.Rank, addActivity.ActivityType) - if e != nil { - return nil, errors.InternalServer(reason.DatabaseError).WithError(e).WithStack() - } - if reachStandard { - addActivity.Rank = 0 - } - } - - if exists { - if _, e = session.Where("id = ?", existsActivity.ID).Cols("`cancelled`"). - Update(&entity.Activity{Cancelled: entity.ActivityAvailable}); e != nil { - return nil, errors.InternalServer(reason.DatabaseError).WithError(e).WithStack() - } - } else { - if _, e = session.Insert(addActivity); e != nil { - return nil, errors.InternalServer(reason.DatabaseError).WithError(e).WithStack() - } - } +func (ar *AnswerActivityRepo) rollbackUserRank(ctx context.Context, session *xorm.Session, + activities []*entity.Activity, + userInfoMapping map[string]*entity.User) (err error) { + for _, act := range activities { + if act.Rank == 0 { + continue + } + user := userInfoMapping[act.UserID] + if user == nil { + continue + } + if err = ar.userRankRepo.ChangeUserRank(ctx, session, + act.UserID, user.Rank, -act.Rank); err != nil { + log.Error(err) + return err } - return nil, nil - }) - if err != nil { - return err } - for _, act := range addActivityList { + return nil +} + +func (ar *AnswerActivityRepo) getExistActivity(ctx context.Context, op *schema.AcceptAnswerOperationInfo) ([]*entity.Activity, error) { + var activities []*entity.Activity + for _, action := range op.Activities { + t := &entity.Activity{} + exist, err := ar.data.DB.Context(ctx). + Where(builder.Eq{"user_id": action.ActivityUserID}). + And(builder.Eq{"trigger_user_id": action.TriggerUserID}). + And(builder.Eq{"activity_type": action.ActivityType}). + And(builder.Eq{"object_id": op.AnswerObjectID}). + Get(t) + if err != nil { + return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + if exist { + activities = append(activities, t) + } + } + return activities, nil +} + +func (ar *AnswerActivityRepo) sendAcceptAnswerNotification( + ctx context.Context, op *schema.AcceptAnswerOperationInfo) { + for _, act := range op.Activities { msg := &schema.NotificationMsg{ Type: schema.NotificationTypeAchievement, - ObjectID: act.ObjectID, - ReceiverUserID: act.UserID, + ObjectID: op.AnswerObjectID, + ReceiverUserID: act.ActivityUserID, } - if act.UserID == questionUserID { - msg.TriggerUserID = answerUserID + if act.ActivityUserID == op.QuestionUserID { + msg.TriggerUserID = op.AnswerUserID msg.ObjectType = constant.AnswerObjectType } else { - msg.TriggerUserID = questionUserID + msg.TriggerUserID = op.QuestionUserID msg.ObjectType = constant.AnswerObjectType } if msg.TriggerUserID != msg.ReceiverUserID { - notice_queue.AddNotification(msg) + ar.notificationQueueService.Send(ctx, msg) } } - for _, act := range addActivityList { + for _, act := range op.Activities { msg := &schema.NotificationMsg{ - ReceiverUserID: act.UserID, + ReceiverUserID: act.ActivityUserID, Type: schema.NotificationTypeInbox, - ObjectID: act.ObjectID, + ObjectID: op.AnswerObjectID, } - if act.UserID != questionUserID { - msg.TriggerUserID = questionUserID + if act.ActivityUserID != op.QuestionUserID { + msg.TriggerUserID = op.QuestionUserID msg.ObjectType = constant.AnswerObjectType msg.NotificationAction = constant.NotificationAcceptAnswer - notice_queue.AddNotification(msg) + ar.notificationQueueService.Send(ctx, msg) } } - return err } -// CancelAcceptAnswer accept other answer -func (ar *AnswerActivityRepo) CancelAcceptAnswer(ctx context.Context, - answerObjID, questionObjID, questionUserID, answerUserID string, -) (err error) { - addActivityList := make([]*entity.Activity, 0) - for _, action := range acceptActionList { - // get accept answer need add rank amount - activityType, deltaRank, hasRank, e := ar.activityRepo.GetActivityTypeByObjID(ctx, answerObjID, action) - if e != nil { - return errors.InternalServer(reason.DatabaseError).WithError(e).WithStack() - } - addActivity := &entity.Activity{ - ObjectID: answerObjID, - ActivityType: activityType, - Rank: -deltaRank, - HasRank: hasRank, - } - if action == acceptAction { - addActivity.UserID = questionUserID - addActivity.OriginalObjectID = questionObjID - } else { - addActivity.UserID = answerUserID - addActivity.OriginalObjectID = answerObjID - } - addActivityList = append(addActivityList, addActivity) - } - - _, err = ar.data.DB.Transaction(func(session *xorm.Session) (result any, err error) { - session = session.Context(ctx) - for _, addActivity := range addActivityList { - existsActivity, exists, e := ar.activityRepo.GetActivity( - ctx, session, answerObjID, addActivity.UserID, addActivity.ActivityType) - if e != nil { - return nil, errors.InternalServer(reason.DatabaseError).WithError(e).WithStack() - } - if exists && existsActivity.Cancelled == entity.ActivityCancelled { - continue - } - if !exists { - continue - } - - if existsActivity.Rank != 0 { - _, e = ar.userRankRepo.TriggerUserRank( - ctx, session, addActivity.UserID, addActivity.Rank, addActivity.ActivityType) - if e != nil { - return nil, errors.InternalServer(reason.DatabaseError).WithError(e).WithStack() - } - } - - if _, e := session.Where("id = ?", existsActivity.ID).Cols("cancelled", "cancelled_at"). - Update(&entity.Activity{Cancelled: entity.ActivityCancelled, CancelledAt: time.Now()}); e != nil { - return nil, errors.InternalServer(reason.DatabaseError).WithError(e).WithStack() - } - } - return nil, nil - }) - if err != nil { - return err - } - for _, act := range addActivityList { +func (ar *AnswerActivityRepo) sendCancelAcceptAnswerNotification( + ctx context.Context, op *schema.AcceptAnswerOperationInfo) { + for _, act := range op.Activities { msg := &schema.NotificationMsg{ - ReceiverUserID: act.UserID, + ReceiverUserID: act.ActivityUserID, Type: schema.NotificationTypeAchievement, - ObjectID: act.ObjectID, + ObjectID: op.AnswerObjectID, } - if act.UserID == questionUserID { - msg.TriggerUserID = answerUserID + if act.ActivityUserID == op.QuestionObjectID { + msg.TriggerUserID = op.AnswerObjectID msg.ObjectType = constant.QuestionObjectType } else { - msg.TriggerUserID = questionUserID + msg.TriggerUserID = op.QuestionObjectID msg.ObjectType = constant.AnswerObjectType } if msg.TriggerUserID != msg.ReceiverUserID { - notice_queue.AddNotification(msg) + ar.notificationQueueService.Send(ctx, msg) } } - return err -} - -func (ar *AnswerActivityRepo) DeleteAnswer(ctx context.Context, answerID string) (err error) { - answerInfo := &entity.Answer{} - exist, err := ar.data.DB.Context(ctx).Where("id = ?", answerID).Get(answerInfo) - if err != nil { - return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() - } - if !exist { - return nil - } - - // get all this object activity - activityList := make([]*entity.Activity, 0) - session := ar.data.DB.Context(ctx).Where("has_rank = 1") - session.Where("cancelled = ?", entity.ActivityAvailable) - err = session.Find(&activityList, &entity.Activity{ObjectID: answerID}) - if err != nil { - return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() - } - if len(activityList) == 0 { - return nil - } - - log.Infof("answerInfo %s deleted will rollback activity %d", answerID, len(activityList)) - - _, err = ar.data.DB.Transaction(func(session *xorm.Session) (result any, err error) { - session = session.Context(ctx) - for _, act := range activityList { - log.Infof("user %s rollback rank %d", act.UserID, -act.Rank) - _, e := ar.userRankRepo.TriggerUserRank( - ctx, session, act.UserID, -act.Rank, act.ActivityType) - if e != nil { - return nil, errors.InternalServer(reason.DatabaseError).WithError(e).WithStack() - } - - if _, e := session.Where("id = ?", act.ID).Cols("cancelled", "cancelled_at"). - Update(&entity.Activity{Cancelled: entity.ActivityCancelled, CancelledAt: time.Now()}); e != nil { - return nil, errors.InternalServer(reason.DatabaseError).WithError(e).WithStack() - } - } - return nil, nil - }) - if err != nil { - return err - } - return } diff --git a/internal/repo/activity/follow_repo.go b/internal/repo/activity/follow_repo.go index 8f9ade89..02f025cd 100644 --- a/internal/repo/activity/follow_repo.go +++ b/internal/repo/activity/follow_repo.go @@ -43,7 +43,7 @@ func (ar *FollowRepo) Follow(ctx context.Context, objectID, userID string) error if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } - activityType, err := ar.activityRepo.GetActivityTypeByObjKey(ctx, objectTypeStr, "follow") + activityType, err := ar.activityRepo.GetActivityTypeByObjectType(ctx, objectTypeStr, "follow") if err != nil { return err } @@ -110,7 +110,7 @@ func (ar *FollowRepo) FollowCancel(ctx context.Context, objectID, userID string) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } - activityType, err := ar.activityRepo.GetActivityTypeByObjKey(ctx, objectTypeStr, "follow") + activityType, err := ar.activityRepo.GetActivityTypeByObjectType(ctx, objectTypeStr, "follow") if err != nil { return err } diff --git a/internal/repo/activity/user_active_repo.go b/internal/repo/activity/user_active_repo.go index 0f6099d1..06c587c3 100644 --- a/internal/repo/activity/user_active_repo.go +++ b/internal/repo/activity/user_active_repo.go @@ -2,6 +2,8 @@ package activity import ( "context" + "fmt" + "xorm.io/builder" "github.com/answerdev/answer/internal/base/data" "github.com/answerdev/answer/internal/base/reason" @@ -41,43 +43,58 @@ func NewUserActiveActivityRepo( } } -// UserActive accept other answer +// UserActive user active func (ar *UserActiveActivityRepo) UserActive(ctx context.Context, userID string) (err error) { cfg, err := ar.configService.GetConfigByKey(ctx, UserActivated) if err != nil { return err } - activityType := cfg.ID - deltaRank := cfg.GetIntValue() addActivity := &entity.Activity{ UserID: userID, ObjectID: "0", OriginalObjectID: "0", - ActivityType: activityType, - Rank: deltaRank, + ActivityType: cfg.ID, + Rank: cfg.GetIntValue(), HasRank: 1, } _, err = ar.data.DB.Transaction(func(session *xorm.Session) (result any, err error) { session = session.Context(ctx) - _, exists, err := ar.activityRepo.GetActivity(ctx, session, "0", addActivity.UserID, activityType) + user := &entity.User{} + exist, err := session.ID(userID).ForUpdate().Get(user) if err != nil { - return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + return nil, err } - if exists { + if !exist { + return nil, fmt.Errorf("user not exist") + } + + existsActivity := &entity.Activity{} + exist, err = session. + And(builder.Eq{"user_id": addActivity.UserID}). + And(builder.Eq{"activity_type": addActivity.ActivityType}). + Get(existsActivity) + if err != nil { + return nil, err + } + if exist { return nil, nil } - _, err = ar.userRankRepo.TriggerUserRank(ctx, session, addActivity.UserID, addActivity.Rank, activityType) + err = ar.userRankRepo.ChangeUserRank(ctx, session, addActivity.UserID, user.Rank, addActivity.Rank) if err != nil { - return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + return nil, err } + _, err = session.Insert(addActivity) if err != nil { - return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + return nil, err } return nil, nil }) - return err + if err != nil { + return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return nil } diff --git a/internal/repo/activity/vote_repo.go b/internal/repo/activity/vote_repo.go index 58bf1510..04d56053 100644 --- a/internal/repo/activity/vote_repo.go +++ b/internal/repo/activity/vote_repo.go @@ -2,395 +2,174 @@ package activity import ( "context" - "strings" + "fmt" + "github.com/segmentfault/pacman/log" "time" "github.com/answerdev/answer/internal/base/constant" + "github.com/answerdev/answer/internal/service/notice_queue" "github.com/answerdev/answer/pkg/converter" "github.com/answerdev/answer/internal/base/pager" - "github.com/answerdev/answer/internal/service/config" - "github.com/answerdev/answer/internal/service/notice_queue" "github.com/answerdev/answer/internal/service/rank" "github.com/answerdev/answer/pkg/obj" "xorm.io/builder" - "github.com/answerdev/answer/internal/service/activity_common" - "github.com/answerdev/answer/internal/service/unique" - "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/schema" "github.com/answerdev/answer/internal/service" + "github.com/answerdev/answer/internal/service/activity_common" "github.com/segmentfault/pacman/errors" "xorm.io/xorm" ) // VoteRepo activity repository type VoteRepo struct { - data *data.Data - uniqueIDRepo unique.UniqueIDRepo - configService *config.ConfigService - activityRepo activity_common.ActivityRepo - userRankRepo rank.UserRankRepo - voteCommon activity_common.VoteRepo + data *data.Data + activityRepo activity_common.ActivityRepo + userRankRepo rank.UserRankRepo + notificationQueueService notice_queue.NotificationQueueService } // NewVoteRepo new repository func NewVoteRepo( data *data.Data, - uniqueIDRepo unique.UniqueIDRepo, - configService *config.ConfigService, activityRepo activity_common.ActivityRepo, userRankRepo rank.UserRankRepo, - voteCommon activity_common.VoteRepo, + notificationQueueService notice_queue.NotificationQueueService, ) service.VoteRepo { return &VoteRepo{ - data: data, - uniqueIDRepo: uniqueIDRepo, - configService: configService, - activityRepo: activityRepo, - userRankRepo: userRankRepo, - voteCommon: voteCommon, + data: data, + activityRepo: activityRepo, + userRankRepo: userRankRepo, + notificationQueueService: notificationQueueService, } } -var LimitUpActions = map[string][]string{ - "question": {"vote_up", "voted_up"}, - "answer": {"vote_up", "voted_up"}, - "comment": {"vote_up"}, -} +func (vr *VoteRepo) Vote(ctx context.Context, op *schema.VoteOperationInfo) (err error) { + noNeedToVote, err := vr.votePreCheck(ctx, op) + if err != nil { + return err + } + if noNeedToVote { + return nil + } -var LimitDownActions = map[string][]string{ - "question": {"vote_down", "voted_down"}, - "answer": {"vote_down", "voted_down"}, - "comment": {"vote_down"}, -} - -func (vr *VoteRepo) vote(ctx context.Context, objectID string, userID, objectUserID string, actions []string) (resp *schema.VoteResp, err error) { - resp = &schema.VoteResp{} - achievementNotificationUserIDs := make([]string, 0) sendInboxNotification := false - upVote := false + maxDailyRank, err := vr.userRankRepo.GetMaxDailyRank(ctx) + if err != nil { + return err + } + var userIDs []string + for _, activity := range op.Activities { + userIDs = append(userIDs, activity.ActivityUserID) + } + _, err = vr.data.DB.Transaction(func(session *xorm.Session) (result any, err error) { session = session.Context(ctx) - result = nil - for _, action := range actions { - var ( - existsActivity entity.Activity - insertActivity entity.Activity - has bool - triggerUserID, - activityUserID string - activityType, deltaRank, hasRank int - ) - activityUserID, activityType, deltaRank, hasRank, err = vr.CheckRank(ctx, objectID, objectUserID, userID, action) - if err != nil { - return - } - - triggerUserID = userID - if userID == activityUserID { - triggerUserID = "0" - } - - // check is voted up - has, _ = session. - Where(builder.Eq{"object_id": objectID}). - And(builder.Eq{"user_id": activityUserID}). - And(builder.Eq{"trigger_user_id": triggerUserID}). - And(builder.Eq{"activity_type": activityType}). - Get(&existsActivity) - - // is is voted,return - if has && existsActivity.Cancelled == entity.ActivityAvailable { - return - } - - insertActivity = entity.Activity{ - ObjectID: objectID, - OriginalObjectID: objectID, - UserID: activityUserID, - TriggerUserID: converter.StringToInt64(triggerUserID), - ActivityType: activityType, - Rank: deltaRank, - HasRank: hasRank, - Cancelled: entity.ActivityAvailable, - } - - // trigger user rank and send notification - if hasRank != 0 { - var isReachStandard bool - isReachStandard, err = vr.userRankRepo.TriggerUserRank(ctx, session, activityUserID, deltaRank, activityType) - if err != nil { - return nil, err - } - if isReachStandard { - insertActivity.Rank = 0 - } - achievementNotificationUserIDs = append(achievementNotificationUserIDs, activityUserID) - } - - if has { - if _, err = session.Where("id = ?", existsActivity.ID).Cols("`cancelled`"). - Update(&entity.Activity{ - Cancelled: entity.ActivityAvailable, - }); err != nil { - return - } - } else { - _, err = session.Insert(&insertActivity) - if err != nil { - return nil, err - } - sendInboxNotification = true - } - - // update votes - if action == constant.ActVoteDown || action == constant.ActVoteUp { - votes := 1 - if action == constant.ActVoteDown { - upVote = false - votes = -1 - } else { - upVote = true - } - err = vr.updateVotes(ctx, session, objectID, votes) - if err != nil { - return - } - } + userInfoMapping, err := vr.acquireUserInfo(session, userIDs) + if err != nil { + return nil, err } - return + + err = vr.setActivityRankToZeroIfUserReachLimit(ctx, session, op, userInfoMapping, maxDailyRank) + if err != nil { + return nil, err + } + + sendInboxNotification, err = vr.saveActivitiesAvailable(session, op) + if err != nil { + return nil, err + } + + err = vr.changeUserRank(ctx, session, op, userInfoMapping) + if err != nil { + return nil, err + } + return nil, nil }) if err != nil { - return + return err } - resp, err = vr.GetVoteResultByObjectId(ctx, objectID) - resp.VoteStatus = vr.voteCommon.GetVoteStatus(ctx, objectID, userID) - - for _, activityUserID := range achievementNotificationUserIDs { - vr.sendNotification(ctx, activityUserID, objectUserID, objectID) + for _, activity := range op.Activities { + if activity.Rank == 0 { + continue + } + vr.sendAchievementNotification(ctx, activity.ActivityUserID, op.ObjectCreatorUserID, op.ObjectID) } if sendInboxNotification { - vr.sendVoteInboxNotification(userID, objectUserID, objectID, upVote) + vr.sendVoteInboxNotification(ctx, op.OperatingUserID, op.ObjectCreatorUserID, op.ObjectID, op.VoteUp) } - return + return nil } -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) +func (vr *VoteRepo) CancelVote(ctx context.Context, op *schema.VoteOperationInfo) (err error) { + // Pre-Check + // 1. check if the activity exist + // 2. check if the activity is not cancelled + // 3. if all activities are cancelled, return directly + activities, err := vr.getExistActivity(ctx, op) + if err != nil { + return err + } + var userIDs []string + for _, activity := range activities { + if activity.Cancelled == entity.ActivityCancelled { + continue + } + userIDs = append(userIDs, activity.UserID) + } + if len(userIDs) == 0 { + return nil + } + _, err = vr.data.DB.Transaction(func(session *xorm.Session) (result any, err error) { session = session.Context(ctx) - for _, action := range actions { - var ( - existsActivity entity.Activity - has bool - triggerUserID, - activityUserID string - activityType, - deltaRank, hasRank int - ) - result = nil - activityUserID, activityType, deltaRank, hasRank, err = vr.CheckRank(ctx, objectID, objectUserID, userID, action) - if err != nil { - return - } - - triggerUserID = userID - if userID == activityUserID { - triggerUserID = "0" - } - - has, err = session. - Where(builder.Eq{"user_id": activityUserID}). - And(builder.Eq{"trigger_user_id": triggerUserID}). - And(builder.Eq{"activity_type": activityType}). - And(builder.Eq{"object_id": objectID}). - Get(&existsActivity) - - if !has { - return - } - - if existsActivity.Cancelled == entity.ActivityCancelled { - return - } - - if _, err = session.Where("id = ?", existsActivity.ID).Cols("cancelled", "cancelled_at"). - Update(&entity.Activity{ - Cancelled: entity.ActivityCancelled, - CancelledAt: time.Now(), - }); err != nil { - return - } - - // trigger user rank and send notification - if hasRank != 0 && existsActivity.Rank != 0 { - _, err = vr.userRankRepo.TriggerUserRank(ctx, session, activityUserID, -deltaRank, activityType) - if err != nil { - return - } - notificationUserIDs = append(notificationUserIDs, activityUserID) - } - - // update votes - if action == "vote_down" || action == "vote_up" { - votes := -1 - if action == "vote_down" { - votes = 1 - } - err = vr.updateVotes(ctx, session, objectID, votes) - if err != nil { - return - } - } + userInfoMapping, err := vr.acquireUserInfo(session, userIDs) + if err != nil { + return nil, err } - return + err = vr.cancelActivities(session, activities) + if err != nil { + return nil, err + } + + err = vr.rollbackUserRank(ctx, session, activities, userInfoMapping) + if err != nil { + return nil, err + } + return nil, nil }) if err != nil { - return + return err } - resp, err = vr.GetVoteResultByObjectId(ctx, objectID) - resp.VoteStatus = vr.voteCommon.GetVoteStatus(ctx, objectID, userID) - for _, activityUserID := range notificationUserIDs { - vr.sendNotification(ctx, activityUserID, objectUserID, objectID) + for _, activity := range activities { + if activity.Rank == 0 { + continue + } + vr.sendAchievementNotification(ctx, activity.UserID, op.ObjectCreatorUserID, op.ObjectID) } + return nil +} + +func (vr *VoteRepo) GetAndSaveVoteResult(ctx context.Context, objectID, objectType string) ( + up, down int64, err error) { + up = vr.countVoteUp(ctx, objectID, objectType) + down = vr.countVoteDown(ctx, objectID, objectType) + err = vr.updateVotes(ctx, objectID, objectType, int(up-down)) return } -func (vr *VoteRepo) VoteUp(ctx context.Context, objectID string, userID, objectUserID string) (resp *schema.VoteResp, err error) { - resp = &schema.VoteResp{} - objectType, err := obj.GetObjectTypeStrByObjectID(objectID) - if err != nil { - err = errors.BadRequest(reason.ObjectNotFound) - return - } - - actions, ok := LimitUpActions[objectType] - if !ok { - err = errors.BadRequest(reason.DisallowVote) - return - } - - _, _ = vr.VoteDownCancel(ctx, objectID, userID, objectUserID) - return vr.vote(ctx, objectID, userID, objectUserID, actions) -} - -func (vr *VoteRepo) VoteDown(ctx context.Context, objectID string, userID, objectUserID string) (resp *schema.VoteResp, err error) { - resp = &schema.VoteResp{} - objectType, err := obj.GetObjectTypeStrByObjectID(objectID) - if err != nil { - err = errors.BadRequest(reason.ObjectNotFound) - return - } - actions, ok := LimitDownActions[objectType] - if !ok { - err = errors.BadRequest(reason.DisallowVote) - return - } - - _, _ = vr.VoteUpCancel(ctx, objectID, userID, objectUserID) - return vr.vote(ctx, objectID, userID, objectUserID, actions) -} - -func (vr *VoteRepo) VoteUpCancel(ctx context.Context, objectID string, userID, objectUserID string) (resp *schema.VoteResp, err error) { - var objectType string - resp = &schema.VoteResp{} - - objectType, err = obj.GetObjectTypeStrByObjectID(objectID) - if err != nil { - err = errors.BadRequest(reason.ObjectNotFound) - return - } - actions, ok := LimitUpActions[objectType] - if !ok { - err = errors.BadRequest(reason.DisallowVote) - return - } - - return vr.voteCancel(ctx, objectID, userID, objectUserID, actions) -} - -func (vr *VoteRepo) VoteDownCancel(ctx context.Context, objectID string, userID, objectUserID string) (resp *schema.VoteResp, err error) { - var objectType string - resp = &schema.VoteResp{} - - objectType, err = obj.GetObjectTypeStrByObjectID(objectID) - if err != nil { - err = errors.BadRequest(reason.ObjectNotFound) - return - } - actions, ok := LimitDownActions[objectType] - if !ok { - err = errors.BadRequest(reason.DisallowVote) - return - } - - return vr.voteCancel(ctx, objectID, userID, objectUserID, actions) -} - -func (vr *VoteRepo) CheckRank(ctx context.Context, objectID, objectUserID, userID string, action string) (activityUserID string, activityType, rank, hasRank int, err error) { - activityType, rank, hasRank, err = vr.activityRepo.GetActivityTypeByObjID(ctx, objectID, action) - - if err != nil { - return - } - - activityUserID = userID - if strings.Contains(action, "voted") { - activityUserID = objectUserID - } - - return activityUserID, activityType, rank, hasRank, nil -} - -func (vr *VoteRepo) GetVoteResultByObjectId(ctx context.Context, objectID string) (resp *schema.VoteResp, err error) { - resp = &schema.VoteResp{} - for _, action := range []string{"vote_up", "vote_down"} { - var ( - activity entity.Activity - votes int64 - activityType int - ) - - activityType, _, _, _ = vr.activityRepo.GetActivityTypeByObjID(ctx, objectID, action) - - votes, err = vr.data.DB.Context(ctx).Where(builder.Eq{"object_id": objectID}). - And(builder.Eq{"activity_type": activityType}). - And(builder.Eq{"cancelled": 0}). - Count(&activity) - - if err != nil { - return - } - - if action == "vote_up" { - resp.UpVotes = int(votes) - } else { - resp.DownVotes = int(votes) - } - } - - resp.Votes = resp.UpVotes - resp.DownVotes - - return resp, nil -} - -func (vr *VoteRepo) ListUserVotes( - ctx context.Context, - userID string, - req schema.GetVoteWithPageReq, - activityTypes []int, -) (voteList []entity.Activity, total int64, err error) { +func (vr *VoteRepo) ListUserVotes(ctx context.Context, userID string, + page int, pageSize int, activityTypes []int) (voteList []*entity.Activity, total int64, err error) { session := vr.data.DB.Context(ctx) cond := builder. And( @@ -399,46 +178,259 @@ func (vr *VoteRepo) ListUserVotes( builder.In("activity_type", activityTypes), ) - session.Where(cond).OrderBy("updated_at desc") + session.Where(cond).Desc("updated_at") - total, err = pager.Help(req.Page, req.PageSize, &voteList, &entity.Activity{}, session) + total, err = pager.Help(page, pageSize, &voteList, &entity.Activity{}, session) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } -// updateVotes -// if votes < 0 Decr object vote_count,otherwise Incr object vote_count -func (vr *VoteRepo) updateVotes(ctx context.Context, session *xorm.Session, objectID string, votes int) (err error) { - var ( - objectType string - e error - ) +func (vr *VoteRepo) votePreCheck(ctx context.Context, op *schema.VoteOperationInfo) (noNeedToVote bool, err error) { + activities, err := vr.getExistActivity(ctx, op) + if err != nil { + return false, err + } + done := 0 + for _, activity := range activities { + if activity.Cancelled == entity.ActivityAvailable { + done++ + } + } + return done == len(op.Activities), nil +} - objectType, err = obj.GetObjectTypeStrByObjectID(objectID) +func (vr *VoteRepo) acquireUserInfo(session *xorm.Session, userIDs []string) (map[string]*entity.User, error) { + us := make([]*entity.User, 0) + err := session.In("id", userIDs).ForUpdate().Find(&us) + if err != nil { + log.Error(err) + return nil, err + } + + users := make(map[string]*entity.User, 0) + for _, u := range us { + users[u.ID] = u + } + return users, nil +} + +func (vr *VoteRepo) setActivityRankToZeroIfUserReachLimit(ctx context.Context, session *xorm.Session, + op *schema.VoteOperationInfo, userInfoMapping map[string]*entity.User, maxDailyRank int) (err error) { + // check if user reach daily rank limit + for _, activity := range op.Activities { + if activity.Rank > 0 { + // check if reach max daily rank + reach, err := vr.userRankRepo.CheckReachLimit(ctx, session, activity.ActivityUserID, maxDailyRank) + if err != nil { + log.Error(err) + return err + } + if reach { + activity.Rank = 0 + continue + } + } else { + // If user rank is lower than 1 after this action, then user rank will be set to 1 only. + userCurrentScore := userInfoMapping[activity.ActivityUserID].Rank + if userCurrentScore+activity.Rank < 1 { + activity.Rank = 1 - userCurrentScore + } + } + } + return nil +} + +func (vr *VoteRepo) changeUserRank(ctx context.Context, session *xorm.Session, + op *schema.VoteOperationInfo, + userInfoMapping map[string]*entity.User) (err error) { + for _, activity := range op.Activities { + if activity.Rank == 0 { + continue + } + user := userInfoMapping[activity.ActivityUserID] + if user == nil { + continue + } + if err = vr.userRankRepo.ChangeUserRank(ctx, session, + activity.ActivityUserID, user.Rank, activity.Rank); err != nil { + log.Error(err) + return err + } + } + return nil +} + +func (vr *VoteRepo) rollbackUserRank(ctx context.Context, session *xorm.Session, + activities []*entity.Activity, + userInfoMapping map[string]*entity.User) (err error) { + for _, activity := range activities { + if activity.Rank == 0 { + continue + } + user := userInfoMapping[activity.UserID] + if user == nil { + continue + } + if err = vr.userRankRepo.ChangeUserRank(ctx, session, + activity.UserID, user.Rank, -activity.Rank); err != nil { + log.Error(err) + return err + } + } + return nil +} + +// saveActivitiesAvailable save activities +// If activity not exist it will be created or else will be updated +// If this activity is already exist, set activity rank to 0 +// So after this function, the activity rank will be correct for update user rank +func (vr *VoteRepo) saveActivitiesAvailable(session *xorm.Session, op *schema.VoteOperationInfo) (newAct bool, err error) { + for _, activity := range op.Activities { + existsActivity := &entity.Activity{} + exist, err := session. + Where(builder.Eq{"object_id": op.ObjectID}). + And(builder.Eq{"user_id": activity.ActivityUserID}). + And(builder.Eq{"trigger_user_id": activity.TriggerUserID}). + And(builder.Eq{"activity_type": activity.ActivityType}). + Get(existsActivity) + if err != nil { + return false, err + } + if exist && existsActivity.Cancelled == entity.ActivityAvailable { + activity.Rank = 0 + continue + } + if exist { + bean := &entity.Activity{ + Cancelled: entity.ActivityAvailable, + Rank: activity.Rank, + HasRank: activity.HasRank(), + } + session.Where("id = ?", existsActivity.ID) + if _, err = session.Cols("`cancelled`", "`rank`", "`has_rank`"). + Update(bean); err != nil { + return false, err + } + } else { + insertActivity := entity.Activity{ + ObjectID: op.ObjectID, + OriginalObjectID: op.ObjectID, + UserID: activity.ActivityUserID, + TriggerUserID: converter.StringToInt64(activity.TriggerUserID), + ActivityType: activity.ActivityType, + Rank: activity.Rank, + HasRank: activity.HasRank(), + Cancelled: entity.ActivityAvailable, + } + _, err = session.Insert(&insertActivity) + if err != nil { + return false, err + } + newAct = true + } + } + return newAct, nil +} + +// cancelActivities cancel activities +// If this activity is already cancelled, set activity rank to 0 +// So after this function, the activity rank will be correct for update user rank +func (vr *VoteRepo) cancelActivities(session *xorm.Session, activities []*entity.Activity) (err error) { + for _, activity := range activities { + t := &entity.Activity{} + exist, err := session.ID(activity.ID).Get(t) + if err != nil { + log.Error(err) + return err + } + if !exist { + log.Error(fmt.Errorf("%s activity not exist", activity.ID)) + return fmt.Errorf("%s activity not exist", activity.ID) + } + // If this activity is already cancelled, set activity rank to 0 + if t.Cancelled == entity.ActivityCancelled { + activity.Rank = 0 + } + if _, err = session.ID(activity.ID).Cols("cancelled", "cancelled_at"). + Update(&entity.Activity{ + Cancelled: entity.ActivityCancelled, + CancelledAt: time.Now(), + }); err != nil { + log.Error(err) + return err + } + } + return nil +} + +func (vr *VoteRepo) getExistActivity(ctx context.Context, op *schema.VoteOperationInfo) ([]*entity.Activity, error) { + var activities []*entity.Activity + for _, action := range op.Activities { + t := &entity.Activity{} + exist, err := vr.data.DB.Context(ctx). + Where(builder.Eq{"user_id": action.ActivityUserID}). + And(builder.Eq{"trigger_user_id": action.TriggerUserID}). + And(builder.Eq{"activity_type": action.ActivityType}). + And(builder.Eq{"object_id": op.ObjectID}). + Get(t) + if err != nil { + return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + if exist { + activities = append(activities, t) + } + } + return activities, nil +} + +func (vr *VoteRepo) countVoteUp(ctx context.Context, objectID, objectType string) (count int64) { + count, err := vr.countVote(ctx, objectID, objectType, constant.ActVoteUp) + if err != nil { + log.Errorf("get vote up count error: %v", err) + } + return count +} + +func (vr *VoteRepo) countVoteDown(ctx context.Context, objectID, objectType string) (count int64) { + count, err := vr.countVote(ctx, objectID, objectType, constant.ActVoteDown) + if err != nil { + log.Errorf("get vote down count error: %v", err) + } + return count +} + +func (vr *VoteRepo) countVote(ctx context.Context, objectID, objectType, action string) (count int64, err error) { + activity := &entity.Activity{} + activityType, _ := vr.activityRepo.GetActivityTypeByObjectType(ctx, objectType, action) + count, err = vr.data.DB.Context(ctx).Where(builder.Eq{"object_id": objectID}). + And(builder.Eq{"activity_type": activityType}). + And(builder.Eq{"cancelled": 0}). + Count(activity) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return count, err +} + +func (vr *VoteRepo) updateVotes(ctx context.Context, objectID, objectType string, voteCount int) (err error) { + session := vr.data.DB.Context(ctx) switch objectType { - case "question": - _, err = session.Where("id = ?", objectID).Incr("vote_count", votes).Update(&entity.Question{}) - case "answer": - _, err = session.Where("id = ?", objectID).Incr("vote_count", votes).Update(&entity.Answer{}) - case "comment": - _, err = session.Where("id = ?", objectID).Incr("vote_count", votes).Update(&entity.Comment{}) - default: - e = errors.BadRequest(reason.DisallowVote) + case constant.QuestionObjectType: + _, err = session.ID(objectID).Cols("vote_count").Update(&entity.Question{VoteCount: voteCount}) + case constant.AnswerObjectType: + _, err = session.ID(objectID).Cols("vote_count").Update(&entity.Answer{VoteCount: voteCount}) + case constant.CommentObjectType: + _, err = session.ID(objectID).Cols("vote_count").Update(&entity.Comment{VoteCount: voteCount}) } - - if e != nil { - err = e - } else if err != nil { - err = errors.BadRequest(reason.DatabaseError).WithError(err).WithStack() + if err != nil { + log.Error(err) } - return } -// sendNotification send rank triggered notification -func (vr *VoteRepo) sendNotification(ctx context.Context, activityUserID, objectUserID, objectID string) { +func (vr *VoteRepo) sendAchievementNotification(ctx context.Context, activityUserID, objectUserID, objectID string) { objectType, err := obj.GetObjectTypeStrByObjectID(objectID) if err != nil { return @@ -451,10 +443,10 @@ func (vr *VoteRepo) sendNotification(ctx context.Context, activityUserID, object ObjectID: objectID, ObjectType: objectType, } - notice_queue.AddNotification(msg) + vr.notificationQueueService.Send(ctx, msg) } -func (vr *VoteRepo) sendVoteInboxNotification(triggerUserID, receiverUserID, objectID string, upvote bool) { +func (vr *VoteRepo) sendVoteInboxNotification(ctx context.Context, triggerUserID, receiverUserID, objectID string, upvote bool) { if triggerUserID == receiverUserID { return } @@ -487,6 +479,6 @@ func (vr *VoteRepo) sendVoteInboxNotification(triggerUserID, receiverUserID, obj } } if len(msg.NotificationAction) > 0 { - notice_queue.AddNotification(msg) + vr.notificationQueueService.Send(ctx, msg) } } diff --git a/internal/repo/activity_common/activity_repo.go b/internal/repo/activity_common/activity_repo.go index a59b7085..ca16e482 100644 --- a/internal/repo/activity_common/activity_repo.go +++ b/internal/repo/activity_common/activity_repo.go @@ -41,12 +41,12 @@ func NewActivityRepo( func (ar *ActivityRepo) GetActivityTypeByObjID(ctx context.Context, objectID string, action string) ( activityType, rank, hasRank int, err error) { - objectKey, err := obj.GetObjectTypeStrByObjectID(objectID) + objectType, err := obj.GetObjectTypeStrByObjectID(objectID) if err != nil { return } - confKey := fmt.Sprintf("%s.%s", objectKey, action) + confKey := fmt.Sprintf("%s.%s", objectType, action) cfg, err := ar.configService.GetConfigByKey(ctx, confKey) if err != nil { return @@ -59,11 +59,11 @@ func (ar *ActivityRepo) GetActivityTypeByObjID(ctx context.Context, objectID str return } -func (ar *ActivityRepo) GetActivityTypeByObjKey(ctx context.Context, objectKey, action string) (activityType int, err error) { - configKey := fmt.Sprintf("%s.%s", objectKey, action) +func (ar *ActivityRepo) GetActivityTypeByObjectType(ctx context.Context, objectType, action string) (activityType int, err error) { + configKey := fmt.Sprintf("%s.%s", objectType, action) cfg, err := ar.configService.GetConfigByKey(ctx, configKey) if err != nil { - err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + return 0, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return cfg.ID, nil } @@ -71,7 +71,7 @@ func (ar *ActivityRepo) GetActivityTypeByObjKey(ctx context.Context, objectKey, func (ar *ActivityRepo) GetActivityTypeByConfigKey(ctx context.Context, configKey string) (activityType int, err error) { cfg, err := ar.configService.GetConfigByKey(ctx, configKey) if err != nil { - err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + return 0, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return cfg.ID, nil } diff --git a/internal/repo/activity_common/follow.go b/internal/repo/activity_common/follow.go index 11b5763c..1c3f08a2 100644 --- a/internal/repo/activity_common/follow.go +++ b/internal/repo/activity_common/follow.go @@ -10,6 +10,7 @@ import ( "github.com/answerdev/answer/internal/service/unique" "github.com/answerdev/answer/pkg/obj" "github.com/segmentfault/pacman/errors" + "github.com/segmentfault/pacman/log" ) // FollowRepo follow repository @@ -71,11 +72,12 @@ func (ar *FollowRepo) GetFollowAmount(ctx context.Context, objectID string) (fol func (ar *FollowRepo) GetFollowUserIDs(ctx context.Context, objectID string) (userIDs []string, err error) { objectTypeStr, err := obj.GetObjectTypeStrByObjectID(objectID) if err != nil { - return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + return nil, err } - activityType, err := ar.activityRepo.GetActivityTypeByObjKey(ctx, objectTypeStr, "follow") + activityType, err := ar.activityRepo.GetActivityTypeByObjectType(ctx, objectTypeStr, "follow") if err != nil { - return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + log.Errorf("can't get activity type by object key: %s", objectTypeStr) + return nil, err } userIDs = make([]string, 0) @@ -94,7 +96,7 @@ func (ar *FollowRepo) GetFollowUserIDs(ctx context.Context, objectID string) (us // GetFollowIDs get all follow id list func (ar *FollowRepo) GetFollowIDs(ctx context.Context, userID, objectKey string) (followIDs []string, err error) { followIDs = make([]string, 0) - activityType, err := ar.activityRepo.GetActivityTypeByObjKey(ctx, objectKey, "follow") + activityType, err := ar.activityRepo.GetActivityTypeByObjectType(ctx, objectKey, "follow") if err != nil { return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } @@ -116,7 +118,7 @@ func (ar *FollowRepo) IsFollowed(ctx context.Context, userID, objectID string) ( return false, err } - activityType, err := ar.activityRepo.GetActivityTypeByObjKey(ctx, objectKey, "follow") + activityType, err := ar.activityRepo.GetActivityTypeByObjectType(ctx, objectKey, "follow") if err != nil { return false, err } diff --git a/internal/repo/answer/answer_repo.go b/internal/repo/answer/answer_repo.go index 76585a07..2cffdb4a 100644 --- a/internal/repo/answer/answer_repo.go +++ b/internal/repo/answer/answer_repo.go @@ -2,14 +2,12 @@ package answer import ( "context" - "strings" + "github.com/answerdev/answer/plugin" "time" - "unicode" - - "xorm.io/builder" "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/pager" "github.com/answerdev/answer/internal/base/reason" "github.com/answerdev/answer/internal/entity" @@ -58,8 +56,11 @@ func (ar *answerRepo) AddAnswer(ctx context.Context, answer *entity.Answer) (err if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } - answer.ID = uid.EnShortID(answer.ID) - answer.QuestionID = uid.EnShortID(answer.QuestionID) + if handler.GetEnableShortID(ctx) { + answer.ID = uid.EnShortID(answer.ID) + answer.QuestionID = uid.EnShortID(answer.QuestionID) + } + _ = ar.updateSearch(ctx, answer.ID) return nil } @@ -74,6 +75,7 @@ func (ar *answerRepo) RemoveAnswer(ctx context.Context, id string) (err error) { if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } + _ = ar.updateSearch(ctx, answer.ID) return nil } @@ -85,6 +87,7 @@ func (ar *answerRepo) UpdateAnswer(ctx context.Context, answer *entity.Answer, C if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } + _ = ar.updateSearch(ctx, answer.ID) return err } @@ -96,6 +99,7 @@ func (ar *answerRepo) UpdateAnswerStatus(ctx context.Context, answer *entity.Ans if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } + _ = ar.updateSearch(ctx, answer.ID) return } @@ -109,9 +113,10 @@ func (ar *answerRepo) GetAnswer(ctx context.Context, id string) ( if err != nil { return nil, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } - answer.ID = uid.EnShortID(answer.ID) - answer.QuestionID = uid.EnShortID(answer.QuestionID) - + if handler.GetEnableShortID(ctx) { + answer.ID = uid.EnShortID(answer.ID) + answer.QuestionID = uid.EnShortID(answer.QuestionID) + } return } @@ -134,9 +139,11 @@ func (ar *answerRepo) GetAnswerList(ctx context.Context, answer *entity.Answer) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } - for _, item := range answerList { - item.ID = uid.EnShortID(item.ID) - item.QuestionID = uid.EnShortID(item.QuestionID) + if handler.GetEnableShortID(ctx) { + for _, item := range answerList { + item.ID = uid.EnShortID(item.ID) + item.QuestionID = uid.EnShortID(item.QuestionID) + } } return } @@ -150,9 +157,11 @@ func (ar *answerRepo) GetAnswerPage(ctx context.Context, page, pageSize int, ans if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } - for _, item := range answerList { - item.ID = uid.EnShortID(item.ID) - item.QuestionID = uid.EnShortID(item.QuestionID) + if handler.GetEnableShortID(ctx) { + for _, item := range answerList { + item.ID = uid.EnShortID(item.ID) + item.QuestionID = uid.EnShortID(item.QuestionID) + } } return } @@ -180,6 +189,7 @@ func (ar *answerRepo) UpdateAccepted(ctx context.Context, id string, questionID return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } } + _ = ar.updateSearch(ctx, id) return nil } @@ -191,8 +201,10 @@ func (ar *answerRepo) GetByID(ctx context.Context, id string) (*entity.Answer, b if err != nil { return &resp, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } - resp.ID = uid.EnShortID(resp.ID) - resp.QuestionID = uid.EnShortID(resp.QuestionID) + if handler.GetEnableShortID(ctx) { + resp.ID = uid.EnShortID(resp.ID) + resp.QuestionID = uid.EnShortID(resp.QuestionID) + } return &resp, has, nil } @@ -222,8 +234,10 @@ func (ar *answerRepo) GetByUserIDQuestionID(ctx context.Context, userID string, if err != nil { return &resp, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } - resp.ID = uid.EnShortID(resp.ID) - resp.QuestionID = uid.EnShortID(resp.QuestionID) + if handler.GetEnableShortID(ctx) { + resp.ID = uid.EnShortID(resp.ID) + resp.QuestionID = uid.EnShortID(resp.QuestionID) + } return &resp, has, nil } @@ -274,87 +288,109 @@ func (ar *answerRepo) SearchList(ctx context.Context, search *entity.AnswerSearc if err != nil { return rows, count, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } - for _, item := range rows { - item.ID = uid.EnShortID(item.ID) - item.QuestionID = uid.EnShortID(item.QuestionID) + if handler.GetEnableShortID(ctx) { + for _, item := range rows { + item.ID = uid.EnShortID(item.ID) + item.QuestionID = uid.EnShortID(item.QuestionID) + } } return rows, count, nil } -func (ar *answerRepo) AdminSearchList(ctx context.Context, search *entity.AdminAnswerSearch) ([]*entity.Answer, int64, error) { - var ( - count int64 - err error - session = ar.data.DB.Context(ctx).Table([]string{entity.Answer{}.TableName(), "a"}).Select("a.*") - ) - if search.QuestionID != "" { - search.QuestionID = uid.DeShortID(search.QuestionID) - } - - session.Where(builder.Eq{ - "a.status": search.Status, - }) - - rows := make([]*entity.Answer, 0) - if search.Page > 0 { - search.Page = search.Page - 1 - } else { - search.Page = 0 - } - if search.PageSize == 0 { - search.PageSize = constant.DefaultPageSize - } - - // search by question title like or answer id - if len(search.Query) > 0 { - // check id search - var ( - idSearch = false - id = "" - ) - - if strings.Contains(search.Query, "answer:") { - idSearch = true - id = strings.TrimSpace(strings.TrimPrefix(search.Query, "answer:")) - id = uid.DeShortID(id) - for _, r := range id { - if !unicode.IsDigit(r) { - idSearch = false - break - } - } - } - - if idSearch { - session.And(builder.Eq{ - "id": id, - }) - } else { - session.Join("LEFT", []string{entity.Question{}.TableName(), "q"}, "q.id = a.question_id") - session.And(builder.Like{ - "q.title", search.Query, - }) +func (ar *answerRepo) AdminSearchList(ctx context.Context, req *schema.AdminAnswerPageReq) ( + resp []*entity.Answer, total int64, err error) { + cond := &entity.Answer{} + session := ar.data.DB.Context(ctx) + if len(req.QuestionID) == 0 && len(req.AnswerID) == 0 { + session.Join("INNER", "question", "answer.question_id = question.id") + if len(req.QuestionTitle) > 0 { + session.Where("question.title like ?", "%"+req.QuestionTitle+"%") } } - - // check search by question id - if len(search.QuestionID) > 0 { - session.And(builder.Eq{ - "question_id": search.QuestionID, - }) + if len(req.AnswerID) > 0 { + cond.ID = req.AnswerID } + if len(req.QuestionID) > 0 { + session.Where("answer.question_id = ?", req.QuestionID) + } + if req.Status > 0 { + cond.Status = req.Status + } + session.Desc("answer.created_at") - offset := search.Page * search.PageSize - session. - OrderBy("a.created_at desc"). - Limit(search.PageSize, offset) - count, err = session.FindAndCount(&rows) + resp = make([]*entity.Answer, 0) + total, err = pager.Help(req.Page, req.PageSize, &resp, cond, session) if err != nil { - return rows, count, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + return nil, 0, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } - for _, item := range rows { - item.ID = uid.EnShortID(item.ID) - item.QuestionID = uid.EnShortID(item.QuestionID) - } - return rows, count, nil + return resp, total, nil +} + +// updateSearch update search, if search plugin not enable, do nothing +func (ar *answerRepo) updateSearch(ctx context.Context, answerID string) (err error) { + answerID = uid.DeShortID(answerID) + // check search plugin + var ( + s plugin.Search + ) + _ = plugin.CallSearch(func(search plugin.Search) error { + s = search + return nil + }) + if s == nil { + return + } + answer, exist, err := ar.GetAnswer(ctx, answerID) + if !exist { + return + } + if err != nil { + return err + } + + // get question + var ( + question *entity.Question + ) + exist, err = ar.data.DB.Context(ctx).Where("id = ?", answer.QuestionID).Get(&question) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + if !exist { + return + } + + // get tags + var ( + tagListList = make([]*entity.TagRel, 0) + tags = make([]string, 0) + ) + st := ar.data.DB.Context(ctx).Where("object_id = ?", uid.DeShortID(question.ID)) + st.Where("status = ?", entity.TagRelStatusAvailable) + err = st.Find(&tagListList) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + for _, tag := range tagListList { + tags = append(tags, tag.TagID) + } + + content := &plugin.SearchContent{ + ObjectID: answerID, + Title: question.Title, + Type: constant.AnswerObjectType, + Content: answer.ParsedText, + Answers: 0, + Status: plugin.SearchContentStatus(answer.Status), + Tags: tags, + QuestionID: answer.QuestionID, + UserID: answer.UserID, + Views: int64(question.ViewCount), + Created: answer.CreatedAt.Unix(), + Active: answer.UpdatedAt.Unix(), + Score: int64(answer.VoteCount), + HasAccepted: answer.Accepted == schema.AnswerAcceptedEnable, + } + err = s.UpdateContent(ctx, answerID, content) + return } diff --git a/internal/repo/auth/auth.go b/internal/repo/auth/auth.go index 5cd808a7..440ec1f8 100644 --- a/internal/repo/auth/auth.go +++ b/internal/repo/auth/auth.go @@ -3,12 +3,12 @@ package auth import ( "context" "encoding/json" + "github.com/answerdev/answer/internal/service/auth" "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/auth" "github.com/segmentfault/pacman/errors" "github.com/segmentfault/pacman/log" ) @@ -18,6 +18,13 @@ type authRepo struct { data *data.Data } +// NewAuthRepo new repository +func NewAuthRepo(data *data.Data) auth.AuthRepo { + return &authRepo{ + data: data, + } +} + // GetUserCacheInfo get user cache info func (ar *authRepo) GetUserCacheInfo(ctx context.Context, accessToken string) (userInfo *entity.UserCacheInfo, err error) { userInfoCache, err := ar.data.Cache.GetString(ctx, constant.UserTokenCacheKey+accessToken) @@ -174,10 +181,3 @@ func (ar *authRepo) RemoveUserTokens(ctx context.Context, userID string, remainT log.Error(err) } } - -// NewAuthRepo new repository -func NewAuthRepo(data *data.Data) auth.AuthRepo { - return &authRepo{ - data: data, - } -} diff --git a/internal/repo/captcha/captcha.go b/internal/repo/captcha/captcha.go index 382e45e7..a332523a 100644 --- a/internal/repo/captcha/captcha.go +++ b/internal/repo/captcha/captcha.go @@ -2,11 +2,13 @@ package captcha import ( "context" + "encoding/json" "fmt" "time" "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/action" "github.com/segmentfault/pacman/errors" "github.com/segmentfault/pacman/log" @@ -24,26 +26,41 @@ func NewCaptchaRepo(data *data.Data) action.CaptchaRepo { } } -func (cr *captchaRepo) SetActionType(ctx context.Context, ip, actionType string, amount int) (err error) { - cacheKey := fmt.Sprintf("ActionRecord:%s@", ip) - err = cr.data.Cache.SetInt64(ctx, cacheKey, int64(amount), 6*time.Minute) +func (cr *captchaRepo) SetActionType(ctx context.Context, unit, actionType, config string, amount int) (err error) { + now := time.Now() + cacheKey := fmt.Sprintf("ActionRecord:%s@%s@%s", unit, actionType, now.Format("2006-1-02")) + value := &entity.ActionRecordInfo{} + value.LastTime = now.Unix() + value.Num = amount + valueStr, err := json.Marshal(value) + if err != nil { + return nil + } + err = cr.data.Cache.SetString(ctx, cacheKey, string(valueStr), 6*time.Minute) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } -func (cr *captchaRepo) GetActionType(ctx context.Context, ip, actionType string) (amount int, err error) { - cacheKey := fmt.Sprintf("ActionRecord:%s@", ip) - res, err := cr.data.Cache.GetInt64(ctx, cacheKey) +func (cr *captchaRepo) GetActionType(ctx context.Context, unit, actionType string) (actioninfo *entity.ActionRecordInfo, err error) { + now := time.Now() + cacheKey := fmt.Sprintf("ActionRecord:%s@%s@%s", unit, actionType, now.Format("2006-1-02")) + actioninfo = &entity.ActionRecordInfo{} + res, err := cr.data.Cache.GetString(ctx, cacheKey) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } - return int(res), nil + err = json.Unmarshal([]byte(res), actioninfo) + if err != nil { + return actioninfo, nil + } + return actioninfo, nil } -func (cr *captchaRepo) DelActionType(ctx context.Context, ip, actionType string) (err error) { - cacheKey := fmt.Sprintf("ActionRecord:%s@", ip) +func (cr *captchaRepo) DelActionType(ctx context.Context, unit, actionType string) (err error) { + now := time.Now() + cacheKey := fmt.Sprintf("ActionRecord:%s@%s@%s", unit, actionType, now.Format("2006-1-02")) err = cr.data.Cache.Del(ctx, cacheKey) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() diff --git a/internal/repo/config/config_repo.go b/internal/repo/config/config_repo.go index 0673b699..0d426d70 100644 --- a/internal/repo/config/config_repo.go +++ b/internal/repo/config/config_repo.go @@ -37,7 +37,7 @@ func (cr configRepo) GetConfigByID(ctx context.Context, id int) (c *entity.Confi } c = &entity.Config{} - exist, err := cr.data.DB.ID(id).Get(c) + exist, err := cr.data.DB.Context(ctx).ID(id).Get(c) if err != nil { return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } diff --git a/internal/repo/provider.go b/internal/repo/provider.go index 10722cec..34fe1c1a 100644 --- a/internal/repo/provider.go +++ b/internal/repo/provider.go @@ -52,7 +52,6 @@ var ProviderSetRepo = wire.NewSet( activity.NewVoteRepo, activity.NewFollowRepo, activity.NewAnswerActivityRepo, - activity.NewQuestionActivityRepo, activity.NewUserActiveActivityRepo, activity.NewActivityRepo, tag.NewTagRepo, diff --git a/internal/repo/question/question_repo.go b/internal/repo/question/question_repo.go index 2932e7ff..eb159628 100644 --- a/internal/repo/question/question_repo.go +++ b/internal/repo/question/question_repo.go @@ -2,11 +2,15 @@ package question import ( "context" + "encoding/json" "fmt" + "github.com/answerdev/answer/plugin" + "github.com/segmentfault/pacman/log" "strings" "time" "unicode" + "github.com/answerdev/answer/internal/base/handler" "xorm.io/builder" "github.com/answerdev/answer/internal/base/constant" @@ -50,7 +54,10 @@ func (qr *questionRepo) AddQuestion(ctx context.Context, question *entity.Questi if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } - question.ID = uid.EnShortID(question.ID) + if handler.GetEnableShortID(ctx) { + question.ID = uid.EnShortID(question.ID) + } + _ = qr.updateSearch(ctx, question.ID) return } @@ -71,7 +78,10 @@ func (qr *questionRepo) UpdateQuestion(ctx context.Context, question *entity.Que if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } - question.ID = uid.EnShortID(question.ID) + if handler.GetEnableShortID(ctx) { + question.ID = uid.EnShortID(question.ID) + } + _ = qr.updateSearch(ctx, question.ID) return } @@ -82,6 +92,7 @@ func (qr *questionRepo) UpdatePvCount(ctx context.Context, questionID string) (e if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } + _ = qr.updateSearch(ctx, question.ID) return nil } @@ -93,6 +104,7 @@ func (qr *questionRepo) UpdateAnswerCount(ctx context.Context, questionID string if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } + _ = qr.updateSearch(ctx, question.ID) return nil } @@ -114,6 +126,7 @@ func (qr *questionRepo) UpdateQuestionStatus(ctx context.Context, question *enti if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } + _ = qr.updateSearch(ctx, question.ID) return nil } @@ -123,6 +136,7 @@ func (qr *questionRepo) UpdateQuestionStatusWithOutUpdateTime(ctx context.Contex if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } + _ = qr.updateSearch(ctx, question.ID) return nil } @@ -141,6 +155,7 @@ func (qr *questionRepo) UpdateAccepted(ctx context.Context, question *entity.Que if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } + _ = qr.updateSearch(ctx, question.ID) return nil } @@ -150,6 +165,7 @@ func (qr *questionRepo) UpdateLastAnswer(ctx context.Context, question *entity.Q if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } + _ = qr.updateSearch(ctx, question.ID) return nil } @@ -164,7 +180,9 @@ func (qr *questionRepo) GetQuestion(ctx context.Context, id string) ( if err != nil { return nil, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } - question.ID = uid.EnShortID(question.ID) + if handler.GetEnableShortID(ctx) { + question.ID = uid.EnShortID(question.ID) + } return } @@ -175,8 +193,10 @@ func (qr *questionRepo) SearchByTitleLike(ctx context.Context, title string) (qu if err != nil { return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } - for _, item := range questionList { - item.ID = uid.EnShortID(item.ID) + if handler.GetEnableShortID(ctx) { + for _, item := range questionList { + item.ID = uid.EnShortID(item.ID) + } } return } @@ -190,8 +210,10 @@ func (qr *questionRepo) FindByID(ctx context.Context, id []string) (questionList if err != nil { return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } - for _, item := range questionList { - item.ID = uid.EnShortID(item.ID) + if handler.GetEnableShortID(ctx) { + for _, item := range questionList { + item.ID = uid.EnShortID(item.ID) + } } return } @@ -211,65 +233,71 @@ func (qr *questionRepo) GetQuestionList(ctx context.Context, question *entity.Qu } func (qr *questionRepo) GetQuestionCount(ctx context.Context) (count int64, err error) { - questionList := make([]*entity.Question, 0) - - count, err = qr.data.DB.Context(ctx).In("question.status", []int{entity.QuestionStatusAvailable, entity.QuestionStatusClosed}).FindAndCount(&questionList) + session := qr.data.DB.Context(ctx) + session.In("status", []int{entity.QuestionStatusAvailable, entity.QuestionStatusClosed}) + count, err = session.Count(&entity.Question{Show: entity.QuestionShow}) if err != nil { - return count, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + return 0, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } - return + return count, nil } func (qr *questionRepo) GetUserQuestionCount(ctx context.Context, userID string) (count int64, err error) { - questionList := make([]*entity.Question, 0) - count, err = qr.data.DB.In("question.status", []int{entity.QuestionStatusAvailable, entity.QuestionStatusClosed}).And("question.user_id = ?", userID).FindAndCount(&questionList) + session := qr.data.DB.Context(ctx) + session.In("status", []int{entity.QuestionStatusAvailable, entity.QuestionStatusClosed}) + count, err = session.Count(&entity.Question{UserID: userID}) if err != nil { return count, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } -func (qr *questionRepo) GetQuestionCountByIDs(ctx context.Context, ids []string) (count int64, err error) { - questionList := make([]*entity.Question, 0) - count, err = qr.data.DB.In("question.status", []int{entity.QuestionStatusAvailable, entity.QuestionStatusClosed}).In("id = ?", ids).Count(&questionList) - if err != nil { - return count, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() - } - return -} - -func (qr *questionRepo) GetQuestionIDsPage(ctx context.Context, page, pageSize int) (questionIDList []*schema.SiteMapQuestionInfo, err error) { +func (qr *questionRepo) SitemapQuestions(ctx context.Context, page, pageSize int) ( + questionIDList []*schema.SiteMapQuestionInfo, err error) { + page = page - 1 questionIDList = make([]*schema.SiteMapQuestionInfo, 0) + + // try to get sitemap data from cache + cacheKey := fmt.Sprintf(constant.SiteMapQuestionCacheKeyPrefix, page) + cacheData, err := qr.data.Cache.GetString(ctx, cacheKey) + if err == nil && len(cacheKey) > 0 { + _ = json.Unmarshal([]byte(cacheData), &questionIDList) + return questionIDList, nil + } + + // get sitemap data from db 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.Context(ctx).Table("question") - session = session.In("question.status", []int{entity.QuestionStatusAvailable, entity.QuestionStatusClosed}) - session.And("question.show = ?", entity.QuestionShow) - session = session.Limit(pageSize, offset) - session = session.OrderBy("question.created_at asc") - err = session.Select("id,title,created_at,post_update_time").Find(&rows) + session := qr.data.DB.Context(ctx) + session.Select("id,title,created_at,post_update_time") + session.Where("`show` = ?", entity.QuestionShow) + session.Where("status = ? OR status = ?", entity.QuestionStatusAvailable, entity.QuestionStatusClosed) + session.Limit(pageSize, page*pageSize) + session.Asc("created_at") + err = session.Find(&rows) if err != nil { return questionIDList, err } + + // warp data for _, question := range rows { - item := &schema.SiteMapQuestionInfo{} - item.ID = uid.EnShortID(question.ID) - item.Title = htmltext.UrlTitle(question.Title) - updateTime := fmt.Sprintf("%v", question.PostUpdateTime.Format(time.RFC3339)) - if question.PostUpdateTime.Unix() < 1 { - updateTime = fmt.Sprintf("%v", question.CreatedAt.Format(time.RFC3339)) + item := &schema.SiteMapQuestionInfo{ID: question.ID} + if handler.GetEnableShortID(ctx) { + item.ID = uid.EnShortID(question.ID) + } + item.Title = htmltext.UrlTitle(question.Title) + if question.PostUpdateTime.IsZero() { + item.UpdateTime = question.CreatedAt.Format(time.RFC3339) + } else { + item.UpdateTime = question.PostUpdateTime.Format(time.RFC3339) } - item.UpdateTime = updateTime questionIDList = append(questionIDList, item) } + + // set sitemap data to cache + cacheDataByte, _ := json.Marshal(questionIDList) + if err := qr.data.Cache.SetString(ctx, cacheKey, string(cacheDataByte), constant.SiteMapQuestionCacheTime); err != nil { + log.Error(err) + } return questionIDList, nil } @@ -312,13 +340,15 @@ func (qr *questionRepo) GetQuestionPage(ctx context.Context, page, pageSize int, if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } - for _, item := range questionList { - item.ID = uid.EnShortID(item.ID) + if handler.GetEnableShortID(ctx) { + for _, item := range questionList { + item.ID = uid.EnShortID(item.ID) + } } return questionList, total, err } -func (qr *questionRepo) AdminSearchList(ctx context.Context, search *schema.AdminQuestionSearch) ([]*entity.Question, int64, error) { +func (qr *questionRepo) AdminQuestionPage(ctx context.Context, search *schema.AdminQuestionPageReq) ([]*entity.Question, int64, error) { var ( count int64 err error @@ -379,8 +409,64 @@ func (qr *questionRepo) AdminSearchList(ctx context.Context, search *schema.Admi err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() return rows, count, err } - for _, item := range rows { - item.ID = uid.EnShortID(item.ID) + if handler.GetEnableShortID(ctx) { + for _, item := range rows { + item.ID = uid.EnShortID(item.ID) + } } return rows, count, nil } + +// updateSearch update search, if search plugin not enable, do nothing +func (qr *questionRepo) updateSearch(ctx context.Context, questionID string) (err error) { + // check search plugin + var s plugin.Search + _ = plugin.CallSearch(func(search plugin.Search) error { + s = search + return nil + }) + if s == nil { + return + } + questionID = uid.DeShortID(questionID) + question, exist, err := qr.GetQuestion(ctx, questionID) + if !exist { + return + } + if err != nil { + return err + } + + // get tags + var ( + tagListList = make([]*entity.TagRel, 0) + tags = make([]string, 0) + ) + session := qr.data.DB.Context(ctx).Where("object_id = ?", questionID) + session.Where("status = ?", entity.TagRelStatusAvailable) + err = session.Find(&tagListList) + if err != nil { + return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + for _, tag := range tagListList { + tags = append(tags, tag.TagID) + } + content := &plugin.SearchContent{ + ObjectID: questionID, + Title: question.Title, + Type: constant.QuestionObjectType, + Content: question.ParsedText, + Answers: int64(question.AnswerCount), + Status: plugin.SearchContentStatus(question.Status), + Tags: tags, + QuestionID: questionID, + UserID: question.UserID, + Views: int64(question.ViewCount), + Created: question.CreatedAt.Unix(), + Active: question.UpdatedAt.Unix(), + Score: int64(question.VoteCount), + HasAccepted: question.AcceptedAnswerID != "" && question.AcceptedAnswerID != "0", + } + err = s.UpdateContent(ctx, questionID, content) + return +} diff --git a/internal/repo/rank/user_rank_repo.go b/internal/repo/rank/user_rank_repo.go index 06570fec..5738c06a 100644 --- a/internal/repo/rank/user_rank_repo.go +++ b/internal/repo/rank/user_rank_repo.go @@ -31,6 +31,56 @@ func NewUserRankRepo(data *data.Data, configService *config.ConfigService) rank. } } +func (ur *UserRankRepo) GetMaxDailyRank(ctx context.Context) (maxDailyRank int, err error) { + maxDailyRank, err = ur.configService.GetIntValue(ctx, "daily_rank_limit") + if err != nil { + return 0, err + } + return maxDailyRank, nil +} + +func (ur *UserRankRepo) CheckReachLimit(ctx context.Context, session *xorm.Session, + userID string, maxDailyRank int) ( + reach bool, err error) { + session.Where(builder.Eq{"user_id": userID}) + session.Where(builder.Eq{"cancelled": 0}) + session.Where(builder.Between{ + Col: "updated_at", + LessVal: now.BeginningOfDay(), + MoreVal: now.EndOfDay(), + }) + + earned, err := session.SumInt(&entity.Activity{}, "`rank`") + if err != nil { + return false, err + } + if int(earned) < maxDailyRank { + return false, nil + } + log.Infof("user %s today has rank %d is reach stand %d", userID, earned, maxDailyRank) + return true, nil +} + +// ChangeUserRank change user rank +func (ur *UserRankRepo) ChangeUserRank( + ctx context.Context, session *xorm.Session, userID string, userCurrentScore, deltaRank int) (err error) { + // IMPORTANT: If user center enabled the rank agent, then we should not change user rank. + if plugin.RankAgentEnabled() || deltaRank == 0 { + return nil + } + + // If user rank is lower than 1 after this action, then user rank will be set to 1 only. + if deltaRank < 0 && userCurrentScore+deltaRank < 1 { + deltaRank = 1 - userCurrentScore + } + + _, err = session.ID(userID).Incr("`rank`", deltaRank).Update(&entity.User{}) + if err != nil { + return err + } + return nil +} + // TriggerUserRank trigger user rank change // session is need provider, it means this action must be success or failure // if outer action is failed then this action is need rollback @@ -38,10 +88,7 @@ func (ur *UserRankRepo) TriggerUserRank(ctx context.Context, session *xorm.Session, userID string, deltaRank int, activityType int, ) (isReachStandard bool, err error) { // IMPORTANT: If user center enabled the rank agent, then we should not change user rank. - if plugin.RankAgentEnabled() { - return false, nil - } - if deltaRank == 0 { + if plugin.RankAgentEnabled() || deltaRank == 0 { return false, nil } @@ -114,7 +161,7 @@ func (ur *UserRankRepo) checkUserTodayRank(ctx context.Context, LessVal: start, MoreVal: end, }) - earned, err := session.Sum(&entity.Activity{}, "rank") + earned, err := session.Sum(&entity.Activity{}, "`rank`") if err != nil { return false, err } @@ -137,7 +184,7 @@ func (ur *UserRankRepo) UserRankPage(ctx context.Context, userID string, page, p ) { rankPage = make([]*entity.Activity, 0) - session := ur.data.DB.Context(ctx).Where(builder.Eq{"has_rank": 1}.And(builder.Eq{"cancelled": 0})).And(builder.Gt{"rank": 0}) + session := ur.data.DB.Context(ctx).Where(builder.Eq{"has_rank": 1}.And(builder.Eq{"cancelled": 0})).And(builder.Gt{"`rank`": 0}) session.Desc("created_at") cond := &entity.Activity{UserID: userID} diff --git a/internal/repo/repo_test/captcha_test.go b/internal/repo/repo_test/captcha_test.go index 74353a8e..fb02c153 100644 --- a/internal/repo/repo_test/captcha_test.go +++ b/internal/repo/repo_test/captcha_test.go @@ -16,7 +16,7 @@ var ( func Test_captchaRepo_DelActionType(t *testing.T) { captchaRepo := captcha.NewCaptchaRepo(testDataSource) - err := captchaRepo.SetActionType(context.TODO(), ip, actionType, amount) + err := captchaRepo.SetActionType(context.TODO(), ip, actionType, "", amount) assert.NoError(t, err) gotAmount, err := captchaRepo.GetActionType(context.TODO(), ip, actionType) diff --git a/internal/repo/repo_test/config_repo_test.go b/internal/repo/repo_test/config_repo_test.go deleted file mode 100644 index 233abc51..00000000 --- a/internal/repo/repo_test/config_repo_test.go +++ /dev/null @@ -1,61 +0,0 @@ -package repo_test - -import ( - "testing" - - "github.com/answerdev/answer/internal/schema" - "github.com/stretchr/testify/assert" -) - -func Test_configRepo_Get(t *testing.T) { - configRepo := config_common.NewConfigRepo(testDataSource) - _, err := configRepo.Get("email.config") - assert.NoError(t, err) -} - -func Test_configRepo_GetArrayString(t *testing.T) { - configRepo := config_common.NewConfigRepo(testDataSource) - got, err := configRepo.GetArrayString("daily_rank_limit.exclude") - assert.NoError(t, err) - assert.Equal(t, 1, len(got)) - assert.Equal(t, "answer.accepted", got[0]) -} - -func Test_configRepo_GetConfigById(t *testing.T) { - configRepo := config_common.NewConfigRepo(testDataSource) - - closeInfo := &schema.GetReportTypeResp{} - err := configRepo.GetJsonConfigByIDAndSetToObject(74, closeInfo) - - assert.NoError(t, err) - assert.Equal(t, "needs close", closeInfo.Name) -} - -func Test_configRepo_GetConfigType(t *testing.T) { - configRepo := config_common.NewConfigRepo(testDataSource) - configType, err := configRepo.GetConfigType("answer.accepted") - assert.NoError(t, err) - assert.Equal(t, 1, configType) -} - -func Test_configRepo_GetInt(t *testing.T) { - configRepo := config_common.NewConfigRepo(testDataSource) - got, err := configRepo.GetInt("answer.accepted") - assert.NoError(t, err) - assert.Equal(t, 15, got) -} - -func Test_configRepo_GetString(t *testing.T) { - configRepo := config_common.NewConfigRepo(testDataSource) - _, err := configRepo.GetString("email.config") - assert.NoError(t, err) -} - -func Test_configRepo_SetConfig(t *testing.T) { - configRepo := config_common.NewConfigRepo(testDataSource) - got, err := configRepo.GetString("email.config") - assert.NoError(t, err) - - err = configRepo.SetConfig("email.config", got) - assert.NoError(t, err) -} diff --git a/internal/repo/repo_test/reason_repo_test.go b/internal/repo/repo_test/reason_repo_test.go index c6092e57..6ce371d3 100644 --- a/internal/repo/repo_test/reason_repo_test.go +++ b/internal/repo/repo_test/reason_repo_test.go @@ -4,13 +4,16 @@ import ( "context" "testing" + "github.com/answerdev/answer/internal/repo/config" + serviceconfig "github.com/answerdev/answer/internal/service/config" + "github.com/answerdev/answer/internal/repo/reason" "github.com/stretchr/testify/assert" ) func Test_reasonRepo_ListReasons(t *testing.T) { - configRepo := config_common.NewConfigRepo(testDataSource) - reasonRepo := reason.NewReasonRepo(configRepo) + configRepo := config.NewConfigRepo(testDataSource) + reasonRepo := reason.NewReasonRepo(serviceconfig.NewConfigService(configRepo)) reasonItems, err := reasonRepo.ListReasons(context.TODO(), "question", "close") assert.NoError(t, err) assert.Equal(t, 4, len(reasonItems)) diff --git a/internal/repo/repo_test/repo_main_test.go b/internal/repo/repo_test/repo_main_test.go index 8fcd8cec..4da4757b 100644 --- a/internal/repo/repo_test/repo_main_test.go +++ b/internal/repo/repo_test/repo_main_test.go @@ -1,6 +1,7 @@ package repo_test import ( + "context" "database/sql" "fmt" "os" @@ -55,6 +56,10 @@ func TestMain(t *testing.M) { // Use sqlite3 to test. dbSetting = dbSettingMapping[string(schemas.SQLITE)] } + if dbSetting.Driver == string(schemas.SQLITE) { + os.RemoveAll(dbSetting.Connection) + } + defer func() { if tearDown != nil { tearDown() @@ -160,8 +165,15 @@ func initDatabase(dbSetting TestDBSetting) (dbEngine *xorm.Engine, err error) { if err != nil { return nil, fmt.Errorf("connection to database failed: %s", err) } - err = migrations.InitDB(dataConf) - if err != nil { + if err := migrations.NewMentor(context.TODO(), dbEngine, &migrations.InitNeedUserInputData{ + Language: "en_US", + SiteName: "ANSWER", + SiteURL: "http://127.0.0.1:8080/", + ContactEmail: "answer@answer.com", + AdminName: "admin", + AdminPassword: "admin", + AdminEmail: "answer@answer.com", + }).InitDB(); err != nil { return nil, fmt.Errorf("migrations init database failed: %s", err) } return dbEngine, nil diff --git a/internal/repo/repo_test/tag_rel_repo_test.go b/internal/repo/repo_test/tag_rel_repo_test.go index 3cb1ffeb..41b6c611 100644 --- a/internal/repo/repo_test/tag_rel_repo_test.go +++ b/internal/repo/repo_test/tag_rel_repo_test.go @@ -2,9 +2,12 @@ package repo_test import ( "context" + "log" "sync" "testing" + "github.com/answerdev/answer/internal/repo/unique" + "github.com/answerdev/answer/internal/entity" "github.com/answerdev/answer/internal/repo/tag" "github.com/stretchr/testify/assert" @@ -14,29 +17,29 @@ var ( tagRelOnce sync.Once testTagRelList = []*entity.TagRel{ { - ObjectID: "1", - TagID: "1", + ObjectID: "10010000000000001", + TagID: "10030000000000001", Status: entity.TagRelStatusAvailable, }, { - ObjectID: "2", - TagID: "2", + ObjectID: "10010000000000002", + TagID: "10030000000000002", Status: entity.TagRelStatusAvailable, }, } ) func addTagRelList() { - tagRelRepo := tag.NewTagRelRepo(testDataSource) + tagRelRepo := tag.NewTagRelRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource)) err := tagRelRepo.AddTagRelList(context.TODO(), testTagRelList) if err != nil { - panic(err) + log.Fatalf("%+v", err) } } func Test_tagListRepo_BatchGetObjectTagRelList(t *testing.T) { tagRelOnce.Do(addTagRelList) - tagRelRepo := tag.NewTagRelRepo(testDataSource) + tagRelRepo := tag.NewTagRelRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource)) relList, err := tagRelRepo.BatchGetObjectTagRelList(context.TODO(), []string{testTagRelList[0].ObjectID, testTagRelList[1].ObjectID}) assert.NoError(t, err) @@ -45,15 +48,15 @@ func Test_tagListRepo_BatchGetObjectTagRelList(t *testing.T) { func Test_tagListRepo_CountTagRelByTagID(t *testing.T) { tagRelOnce.Do(addTagRelList) - tagRelRepo := tag.NewTagRelRepo(testDataSource) - count, err := tagRelRepo.CountTagRelByTagID(context.TODO(), "1") + tagRelRepo := tag.NewTagRelRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource)) + count, err := tagRelRepo.CountTagRelByTagID(context.TODO(), "10030000000000001") assert.NoError(t, err) assert.Equal(t, int64(1), count) } func Test_tagListRepo_GetObjectTagRelList(t *testing.T) { tagRelOnce.Do(addTagRelList) - tagRelRepo := tag.NewTagRelRepo(testDataSource) + tagRelRepo := tag.NewTagRelRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource)) relList, err := tagRelRepo.GetObjectTagRelList(context.TODO(), testTagRelList[0].ObjectID) @@ -63,7 +66,7 @@ func Test_tagListRepo_GetObjectTagRelList(t *testing.T) { func Test_tagListRepo_GetObjectTagRelWithoutStatus(t *testing.T) { tagRelOnce.Do(addTagRelList) - tagRelRepo := tag.NewTagRelRepo(testDataSource) + tagRelRepo := tag.NewTagRelRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource)) relList, err := tagRelRepo.BatchGetObjectTagRelList(context.TODO(), []string{testTagRelList[0].ObjectID, testTagRelList[1].ObjectID}) @@ -74,7 +77,7 @@ func Test_tagListRepo_GetObjectTagRelWithoutStatus(t *testing.T) { err = tagRelRepo.RemoveTagRelListByIDs(context.TODO(), ids) assert.NoError(t, err) - count, err := tagRelRepo.CountTagRelByTagID(context.TODO(), "1") + count, err := tagRelRepo.CountTagRelByTagID(context.TODO(), "10030000000000001") assert.NoError(t, err) assert.Equal(t, int64(0), count) @@ -85,7 +88,7 @@ func Test_tagListRepo_GetObjectTagRelWithoutStatus(t *testing.T) { err = tagRelRepo.EnableTagRelByIDs(context.TODO(), ids) assert.NoError(t, err) - count, err = tagRelRepo.CountTagRelByTagID(context.TODO(), "1") + count, err = tagRelRepo.CountTagRelByTagID(context.TODO(), "10030000000000001") assert.NoError(t, err) assert.Equal(t, int64(1), count) } diff --git a/internal/repo/repo_test/tag_repo_test.go b/internal/repo/repo_test/tag_repo_test.go index f3e636fb..895b1972 100644 --- a/internal/repo/repo_test/tag_repo_test.go +++ b/internal/repo/repo_test/tag_repo_test.go @@ -3,6 +3,7 @@ package repo_test import ( "context" "fmt" + "log" "sync" "testing" @@ -46,7 +47,7 @@ func addTagList() { tagCommonRepo := tag_common.NewTagCommonRepo(testDataSource, uniqueIDRepo) err := tagCommonRepo.AddTagList(context.TODO(), testTagList) if err != nil { - panic(err) + log.Fatalf("%+v", err) } } diff --git a/internal/repo/repo_test/user_repo_test.go b/internal/repo/repo_test/user_repo_test.go index 105f9885..f90c7ae4 100644 --- a/internal/repo/repo_test/user_repo_test.go +++ b/internal/repo/repo_test/user_repo_test.go @@ -10,7 +10,7 @@ import ( ) func Test_userRepo_AddUser(t *testing.T) { - userRepo := user.NewUserRepo(testDataSource, config_common.NewConfigRepo(testDataSource)) + userRepo := user.NewUserRepo(testDataSource) userInfo := &entity.User{ Username: "answer", Pass: "answer", @@ -25,7 +25,7 @@ func Test_userRepo_AddUser(t *testing.T) { } func Test_userRepo_BatchGetByID(t *testing.T) { - userRepo := user.NewUserRepo(testDataSource, config_common.NewConfigRepo(testDataSource)) + userRepo := user.NewUserRepo(testDataSource) got, err := userRepo.BatchGetByID(context.TODO(), []string{"1"}) assert.NoError(t, err) assert.Equal(t, 1, len(got)) @@ -33,7 +33,7 @@ func Test_userRepo_BatchGetByID(t *testing.T) { } func Test_userRepo_GetByEmail(t *testing.T) { - userRepo := user.NewUserRepo(testDataSource, config_common.NewConfigRepo(testDataSource)) + userRepo := user.NewUserRepo(testDataSource) got, exist, err := userRepo.GetByEmail(context.TODO(), "admin@admin.com") assert.NoError(t, err) assert.True(t, exist) @@ -41,7 +41,7 @@ func Test_userRepo_GetByEmail(t *testing.T) { } func Test_userRepo_GetByUserID(t *testing.T) { - userRepo := user.NewUserRepo(testDataSource, config_common.NewConfigRepo(testDataSource)) + userRepo := user.NewUserRepo(testDataSource) got, exist, err := userRepo.GetByUserID(context.TODO(), "1") assert.NoError(t, err) assert.True(t, exist) @@ -49,7 +49,7 @@ func Test_userRepo_GetByUserID(t *testing.T) { } func Test_userRepo_GetByUsername(t *testing.T) { - userRepo := user.NewUserRepo(testDataSource, config_common.NewConfigRepo(testDataSource)) + userRepo := user.NewUserRepo(testDataSource) got, exist, err := userRepo.GetByUsername(context.TODO(), "admin") assert.NoError(t, err) assert.True(t, exist) @@ -57,7 +57,7 @@ func Test_userRepo_GetByUsername(t *testing.T) { } func Test_userRepo_IncreaseAnswerCount(t *testing.T) { - userRepo := user.NewUserRepo(testDataSource, config_common.NewConfigRepo(testDataSource)) + userRepo := user.NewUserRepo(testDataSource) err := userRepo.IncreaseAnswerCount(context.TODO(), "1", 1) assert.NoError(t, err) @@ -68,7 +68,7 @@ func Test_userRepo_IncreaseAnswerCount(t *testing.T) { } func Test_userRepo_IncreaseQuestionCount(t *testing.T) { - userRepo := user.NewUserRepo(testDataSource, config_common.NewConfigRepo(testDataSource)) + userRepo := user.NewUserRepo(testDataSource) err := userRepo.IncreaseQuestionCount(context.TODO(), "1", 1) assert.NoError(t, err) @@ -79,19 +79,19 @@ func Test_userRepo_IncreaseQuestionCount(t *testing.T) { } func Test_userRepo_UpdateEmail(t *testing.T) { - userRepo := user.NewUserRepo(testDataSource, config_common.NewConfigRepo(testDataSource)) + userRepo := user.NewUserRepo(testDataSource) err := userRepo.UpdateEmail(context.TODO(), "1", "admin@admin.com") assert.NoError(t, err) } func Test_userRepo_UpdateEmailStatus(t *testing.T) { - userRepo := user.NewUserRepo(testDataSource, config_common.NewConfigRepo(testDataSource)) + userRepo := user.NewUserRepo(testDataSource) err := userRepo.UpdateEmailStatus(context.TODO(), "1", entity.EmailStatusToBeVerified) assert.NoError(t, err) } func Test_userRepo_UpdateInfo(t *testing.T) { - userRepo := user.NewUserRepo(testDataSource, config_common.NewConfigRepo(testDataSource)) + userRepo := user.NewUserRepo(testDataSource) err := userRepo.UpdateInfo(context.TODO(), &entity.User{ID: "1", Bio: "test"}) assert.NoError(t, err) @@ -102,19 +102,19 @@ func Test_userRepo_UpdateInfo(t *testing.T) { } func Test_userRepo_UpdateLastLoginDate(t *testing.T) { - userRepo := user.NewUserRepo(testDataSource, config_common.NewConfigRepo(testDataSource)) + userRepo := user.NewUserRepo(testDataSource) err := userRepo.UpdateLastLoginDate(context.TODO(), "1") assert.NoError(t, err) } func Test_userRepo_UpdateNoticeStatus(t *testing.T) { - userRepo := user.NewUserRepo(testDataSource, config_common.NewConfigRepo(testDataSource)) + userRepo := user.NewUserRepo(testDataSource) err := userRepo.UpdateNoticeStatus(context.TODO(), "1", 1) assert.NoError(t, err) } func Test_userRepo_UpdatePass(t *testing.T) { - userRepo := user.NewUserRepo(testDataSource, config_common.NewConfigRepo(testDataSource)) + userRepo := user.NewUserRepo(testDataSource) err := userRepo.UpdatePass(context.TODO(), "1", "admin") assert.NoError(t, err) } diff --git a/internal/repo/search_common/search_repo.go b/internal/repo/search_common/search_repo.go index 1f46bdf4..ca659991 100644 --- a/internal/repo/search_common/search_repo.go +++ b/internal/repo/search_common/search_repo.go @@ -3,6 +3,7 @@ package search_common import ( "context" "fmt" + "github.com/answerdev/answer/plugin" "strconv" "strings" "time" @@ -426,6 +427,34 @@ func (sr *searchRepo) parseOrder(ctx context.Context, order string) (res string) return } +// ParseSearchPluginResult parse search plugin result +func (sr *searchRepo) ParseSearchPluginResult(ctx context.Context, sres []plugin.SearchResult) (resp []schema.SearchResp, err error) { + var ( + qres []map[string][]byte + res = make([]map[string][]byte, 0) + b *builder.Builder + ) + for _, r := range sres { + switch r.Type { + case "question": + b = builder.MySQL().Select(qFields...).From("question").Where(builder.Eq{"id": r.ID}). + And(builder.Lt{"`status`": entity.QuestionStatusDeleted}) + case "answer": + b = builder.MySQL().Select(aFields...).From("answer").LeftJoin("`question`", "`question`.`id` = `answer`.`question_id`"). + Where(builder.Eq{"`answer`.`id`": r.ID}). + And(builder.Lt{"`question`.`status`": entity.QuestionStatusDeleted}). + And(builder.Lt{"`answer`.`status`": entity.AnswerStatusDeleted}).And(builder.Eq{"`question`.`show`": entity.QuestionShow}) + } + qres, err = sr.data.DB.Context(ctx).Query(b) + if err != nil || len(qres) == 0 { + continue + } + res = append(res, qres[0]) + } + return sr.parseResult(ctx, res) +} + +// parseResult parse search result, return the data structure func (sr *searchRepo) parseResult(ctx context.Context, res []map[string][]byte) (resp []schema.SearchResp, err error) { for _, r := range res { var ( diff --git a/internal/repo/tag/tag_rel_repo.go b/internal/repo/tag/tag_rel_repo.go index 62b3fe7a..38764915 100644 --- a/internal/repo/tag/tag_rel_repo.go +++ b/internal/repo/tag/tag_rel_repo.go @@ -2,8 +2,8 @@ package tag import ( "context" - "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/entity" tagcommon "github.com/answerdev/answer/internal/service/tag_common" @@ -36,8 +36,10 @@ func (tr *tagRelRepo) AddTagRelList(ctx context.Context, tagList []*entity.TagRe if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } - for _, item := range tagList { - item.ObjectID = uid.EnShortID(item.ObjectID) + if handler.GetEnableShortID(ctx) { + for _, item := range tagList { + item.ObjectID = uid.EnShortID(item.ObjectID) + } } return } @@ -54,7 +56,7 @@ func (tr *tagRelRepo) RemoveTagRelListByObjectID(ctx context.Context, objectID s func (tr *tagRelRepo) HideTagRelListByObjectID(ctx context.Context, objectID string) (err error) { objectID = uid.DeShortID(objectID) - _, err = tr.data.DB.Where("object_id = ?", objectID).Cols("status").Update(&entity.TagRel{Status: entity.TagRelStatusHide}) + _, err = tr.data.DB.Context(ctx).Where("object_id = ?", objectID).Cols("status").Update(&entity.TagRel{Status: entity.TagRelStatusHide}) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } @@ -63,7 +65,7 @@ func (tr *tagRelRepo) HideTagRelListByObjectID(ctx context.Context, objectID str func (tr *tagRelRepo) ShowTagRelListByObjectID(ctx context.Context, objectID string) (err error) { objectID = uid.DeShortID(objectID) - _, err = tr.data.DB.Where("object_id = ?", objectID).Cols("status").Update(&entity.TagRel{Status: entity.TagRelStatusAvailable}) + _, err = tr.data.DB.Context(ctx).Where("object_id = ?", objectID).Cols("status").Update(&entity.TagRel{Status: entity.TagRelStatusAvailable}) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } @@ -89,8 +91,11 @@ func (tr *tagRelRepo) GetObjectTagRelWithoutStatus(ctx context.Context, objectID exist, err = session.Get(tagRel) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + return + } + if handler.GetEnableShortID(ctx) { + tagRel.ObjectID = uid.EnShortID(tagRel.ObjectID) } - tagRel.ObjectID = uid.EnShortID(tagRel.ObjectID) return } @@ -112,9 +117,12 @@ func (tr *tagRelRepo) GetObjectTagRelList(ctx context.Context, objectID string) err = session.Find(&tagListList) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + return } - for _, item := range tagListList { - item.ObjectID = uid.EnShortID(item.ObjectID) + if handler.GetEnableShortID(ctx) { + for _, item := range tagListList { + item.ObjectID = uid.EnShortID(item.ObjectID) + } } return } @@ -130,9 +138,12 @@ func (tr *tagRelRepo) BatchGetObjectTagRelList(ctx context.Context, objectIds [] err = session.Find(&tagListList) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + return } - for _, item := range tagListList { - item.ObjectID = uid.EnShortID(item.ObjectID) + if handler.GetEnableShortID(ctx) { + for _, item := range tagListList { + item.ObjectID = uid.EnShortID(item.ObjectID) + } } return } diff --git a/internal/repo/user/user_repo.go b/internal/repo/user/user_repo.go index 72f7dc5c..e3b35c15 100644 --- a/internal/repo/user/user_repo.go +++ b/internal/repo/user/user_repo.go @@ -2,6 +2,7 @@ package user import ( "context" + "strings" "time" "github.com/answerdev/answer/internal/base/data" @@ -72,7 +73,7 @@ func (ur *userRepo) IncreaseQuestionCount(ctx context.Context, userID string, am func (ur *userRepo) UpdateQuestionCount(ctx context.Context, userID string, count int64) (err error) { user := &entity.User{} user.QuestionCount = int(count) - _, err = ur.data.DB.Where("id = ?", userID).Cols("question_count").Update(user) + _, err = ur.data.DB.Context(ctx).Where("id = ?", userID).Cols("question_count").Update(user) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } @@ -82,7 +83,7 @@ func (ur *userRepo) UpdateQuestionCount(ctx context.Context, userID string, coun func (ur *userRepo) UpdateAnswerCount(ctx context.Context, userID string, count int) (err error) { user := &entity.User{} user.AnswerCount = count - _, err = ur.data.DB.Where("id = ?", userID).Cols("answer_count").Update(user) + _, err = ur.data.DB.Context(ctx).Where("id = ?", userID).Cols("answer_count").Update(user) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } @@ -195,7 +196,7 @@ func (ur *userRepo) GetByUsername(ctx context.Context, username string) (userInf func (ur *userRepo) GetByUsernames(ctx context.Context, usernames []string) ([]*entity.User, error) { list := make([]*entity.User, 0) - err := ur.data.DB.Where("status =?", entity.UserStatusAvailable).In("username", usernames).Find(&list) + err := ur.data.DB.Context(ctx).Where("status =?", entity.UserStatusAvailable).In("username", usernames).Find(&list) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() return list, err @@ -224,18 +225,16 @@ func (ur *userRepo) GetUserCount(ctx context.Context) (count int64, err error) { return } -func (ur *userRepo) SearchUserListByName(ctx context.Context, name string) (userList []*entity.User, err error) { +func (ur *userRepo) SearchUserListByName(ctx context.Context, name string, limit int) (userList []*entity.User, err error) { userList = make([]*entity.User, 0) - if name == "" { - return userList, nil - } - session := ur.data.DB.Where("") - session.Where("username LIKE LOWER(?) or display_name LIKE ?", name+"%", name+"%").And("status =?", entity.UserStatusAvailable) - session.Asc("username") - session = session.Limit(5, 0) - err = session.OrderBy("id desc").Find(&userList) + session := ur.data.DB.Context(ctx) + session.Where("status = ?", entity.UserStatusAvailable) + session.Where("username LIKE ? OR display_name LIKE ?", strings.ToLower(name)+"%", name+"%") + session.OrderBy("username ASC, id DESC") + session.Limit(limit) + err = session.Find(&userList) if err != nil { - err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } tryToDecorateUserListFromUserCenter(ctx, ur.data, userList) return diff --git a/internal/router/answer_api_router.go b/internal/router/answer_api_router.go index 025ee594..e02994a2 100644 --- a/internal/router/answer_api_router.go +++ b/internal/router/answer_api_router.go @@ -26,7 +26,7 @@ type AnswerAPIRouter struct { reasonController *controller.ReasonController themeController *controller_admin.ThemeController siteInfoController *controller_admin.SiteInfoController - siteinfoController *controller.SiteinfoController + siteinfoController *controller.SiteInfoController notificationController *controller.NotificationController dashboardController *controller.DashboardController uploadController *controller.UploadController @@ -55,7 +55,7 @@ func NewAnswerAPIRouter( reasonController *controller.ReasonController, themeController *controller_admin.ThemeController, siteInfoController *controller_admin.SiteInfoController, - siteinfoController *controller.SiteinfoController, + siteinfoController *controller.SiteInfoController, notificationController *controller.NotificationController, dashboardController *controller.DashboardController, uploadController *controller.UploadController, @@ -111,7 +111,6 @@ func (a *AnswerAPIRouter) RegisterMustUnAuthAnswerAPIRouter(r *gin.RouterGroup) routerGroup.GET("/user/register/captcha", a.userController.UserRegisterCaptcha) routerGroup.POST("/user/email/verification", a.userController.UserVerifyEmail) routerGroup.PUT("/user/email", a.userController.UserChangeEmailVerify) - routerGroup.GET("/user/action/record", a.userController.ActionRecord) routerGroup.POST("/user/password/reset", a.userController.RetrievePassWord) routerGroup.POST("/user/password/replacement", a.userController.UseRePassWord) routerGroup.PUT("/user/email/notification", a.userController.UserUnsubscribeEmailNotification) @@ -124,6 +123,7 @@ func (a *AnswerAPIRouter) RegisterUnAuthAnswerAPIRouter(r *gin.RouterGroup) { r.POST("/user/email/verification/send", middleware.BanAPIForUserCenter, a.userController.UserVerifyEmailSend) r.GET("/personal/user/info", a.userController.GetOtherUserInfoByUsername) r.GET("/user/ranking", a.userController.UserRanking) + r.GET("/user/action/record", a.userController.ActionRecord) //answer r.GET("/answer/info", a.answerController.Get) @@ -244,9 +244,9 @@ func (a *AnswerAPIRouter) RegisterAnswerAPIRouter(r *gin.RouterGroup) { } func (a *AnswerAPIRouter) RegisterAnswerAdminAPIRouter(r *gin.RouterGroup) { - r.GET("/question/page", a.questionController.AdminSearchList) + r.GET("/question/page", a.questionController.AdminQuestionPage) r.PUT("/question/status", a.questionController.AdminSetQuestionStatus) - r.GET("/answer/page", a.questionController.AdminSearchAnswerList) + r.GET("/answer/page", a.questionController.AdminAnswerPage) r.PUT("/answer/status", a.answerController.AdminSetAnswerStatus) // report @@ -257,6 +257,8 @@ func (a *AnswerAPIRouter) RegisterAnswerAdminAPIRouter(r *gin.RouterGroup) { r.GET("/users/page", a.adminUserController.GetUserPage) r.PUT("/user/status", a.adminUserController.UpdateUserStatus) r.PUT("/user/role", a.adminUserController.UpdateUserRole) + r.GET("/user/activation", a.adminUserController.GetUserActivation) + r.POST("/user/activation", a.adminUserController.SendUserActivation) r.POST("/user", a.adminUserController.AddUser) r.PUT("/user/password", a.adminUserController.UpdateUserPassword) diff --git a/internal/router/template_router.go b/internal/router/template_router.go index 4ecb99aa..23f1138c 100644 --- a/internal/router/template_router.go +++ b/internal/router/template_router.go @@ -1,6 +1,7 @@ package router import ( + "github.com/answerdev/answer/internal/base/middleware" "github.com/answerdev/answer/internal/controller" templaterender "github.com/answerdev/answer/internal/controller/template_render" "github.com/answerdev/answer/internal/controller_admin" @@ -11,22 +12,25 @@ type TemplateRouter struct { templateController *controller.TemplateController templateRenderController *templaterender.TemplateRenderController siteInfoController *controller_admin.SiteInfoController + authUserMiddleware *middleware.AuthUserMiddleware } func NewTemplateRouter( templateController *controller.TemplateController, templateRenderController *templaterender.TemplateRenderController, siteInfoController *controller_admin.SiteInfoController, + authUserMiddleware *middleware.AuthUserMiddleware, ) *TemplateRouter { return &TemplateRouter{ templateController: templateController, templateRenderController: templateRenderController, siteInfoController: siteInfoController, + authUserMiddleware: authUserMiddleware, } } -// TemplateRouter template router +// RegisterTemplateRouter template router func (a *TemplateRouter) RegisterTemplateRouter(r *gin.RouterGroup) { r.GET("/sitemap.xml", a.templateController.Sitemap) r.GET("/sitemap/:page", a.templateController.SitemapPage) @@ -34,16 +38,17 @@ func (a *TemplateRouter) RegisterTemplateRouter(r *gin.RouterGroup) { 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) + + //todo add middleware + seo := r.Group("") + seo.Use(a.authUserMiddleware.CheckPrivateMode()) + seo.GET("/", a.templateController.Index) + seo.GET("/questions", a.templateController.QuestionList) + seo.GET("/questions/:id", a.templateController.QuestionInfo) + seo.GET("/questions/:id/:title", a.templateController.QuestionInfo) + seo.GET("/questions/:id/:title/:answerid", a.templateController.QuestionInfo) + seo.GET("/tags", a.templateController.TagList) + seo.GET("/tags/:tag", a.templateController.TagInfo) + seo.GET("/users/:username", a.templateController.UserInfo) } diff --git a/internal/router/ui.go b/internal/router/ui.go index f94bcd3f..1e8bf8ec 100644 --- a/internal/router/ui.go +++ b/internal/router/ui.go @@ -21,14 +21,14 @@ const UIStaticPath = "build/static" // UIRouter is an interface that provides ui static file routers type UIRouter struct { - siteInfoController *controller.SiteinfoController - siteInfoService *siteinfo_common.SiteInfoCommonService + siteInfoController *controller.SiteInfoController + siteInfoService siteinfo_common.SiteInfoCommonService } // NewUIRouter creates a new UIRouter instance with the embed resources func NewUIRouter( - siteInfoController *controller.SiteinfoController, - siteInfoService *siteinfo_common.SiteInfoCommonService, + siteInfoController *controller.SiteInfoController, + siteInfoService siteinfo_common.SiteInfoCommonService, ) *UIRouter { return &UIRouter{ siteInfoController: siteInfoController, diff --git a/internal/schema/activity.go b/internal/schema/activity.go index 6e775c88..0dfdf37d 100644 --- a/internal/schema/activity.go +++ b/internal/schema/activity.go @@ -4,12 +4,13 @@ import "github.com/answerdev/answer/internal/base/constant" // ActivityMsg activity message type ActivityMsg struct { - UserID string `json:"user_id"` - TriggerUserID int64 `json:"trigger_user_id"` - ObjectID string `json:"object_id"` - OriginalObjectID string `json:"original_object_id"` - ActivityTypeKey constant.ActivityTypeKey `json:"activity_type_key"` - RevisionID string `json:"revision_id"` + UserID string + TriggerUserID int64 + ObjectID string + OriginalObjectID string + ActivityTypeKey constant.ActivityTypeKey + RevisionID string + ExtraInfo map[string]string } // GetObjectTimelineReq get object timeline request diff --git a/internal/schema/answer_activity_schema.go b/internal/schema/answer_activity_schema.go new file mode 100644 index 00000000..3bf7b39b --- /dev/null +++ b/internal/schema/answer_activity_schema.go @@ -0,0 +1,36 @@ +package schema + +// AcceptAnswerOperationInfo accept answer operation info +type AcceptAnswerOperationInfo struct { + TriggerUserID string + QuestionObjectID string + QuestionUserID string + AnswerObjectID string + AnswerUserID string + + // vote activity info + Activities []*AcceptAnswerActivity +} + +// AcceptAnswerActivity accept answer activity +type AcceptAnswerActivity struct { + ActivityType int + ActivityUserID string + TriggerUserID string + OriginalObjectID string + Rank int +} + +func (v *AcceptAnswerActivity) HasRank() int { + if v.Rank != 0 { + return 1 + } + return 0 +} + +func (a *AcceptAnswerOperationInfo) GetUserIDs() (userIDs []string) { + for _, act := range a.Activities { + userIDs = append(userIDs, act.ActivityUserID) + } + return userIDs +} diff --git a/internal/schema/answer_schema.go b/internal/schema/answer_schema.go index f0487e6a..81465d65 100644 --- a/internal/schema/answer_schema.go +++ b/internal/schema/answer_schema.go @@ -12,7 +12,9 @@ type RemoveAnswerReq struct { // user id UserID string `json:"-"` // whether user can delete it - CanDelete bool `json:"-"` + CanDelete bool `json:"-"` + CaptchaID string `json:"captcha_id"` // captcha_id + CaptchaCode string `json:"captcha_code"` } const ( @@ -21,12 +23,14 @@ const ( ) type AnswerAddReq struct { - QuestionID string `json:"question_id"` - Content string `validate:"required,notblank,gte=6,lte=65535" json:"content"` - HTML string `json:"-"` - UserID string `json:"-"` - CanEdit bool `json:"-"` - CanDelete bool `json:"-"` + QuestionID string `json:"question_id"` + Content string `validate:"required,notblank,gte=6,lte=65535" json:"content"` + HTML string `json:"-"` + UserID string `json:"-"` + CanEdit bool `json:"-"` + CanDelete bool `json:"-"` + CaptchaID string `json:"captcha_id"` // captcha_id + CaptchaCode string `json:"captcha_code"` } func (req *AnswerAddReq) Check() (errFields []*validator.FormErrorField, err error) { @@ -44,7 +48,9 @@ type AnswerUpdateReq struct { UserID string `json:"-"` NoNeedReview bool `json:"-"` // whether user can edit it - CanEdit bool `json:"-"` + CanEdit bool `json:"-"` + CaptchaID string `json:"captcha_id"` // captcha_id + CaptchaCode string `json:"captcha_code"` } func (req *AnswerUpdateReq) Check() (errFields []*validator.FormErrorField, err error) { diff --git a/internal/schema/backyard_user_schema.go b/internal/schema/backyard_user_schema.go index 995f0187..9b34b133 100644 --- a/internal/schema/backyard_user_schema.go +++ b/internal/schema/backyard_user_schema.go @@ -86,7 +86,7 @@ type UpdateUserRoleReq struct { // AddUserReq add user request type AddUserReq struct { - DisplayName string `validate:"required,gt=4,lte=30" json:"display_name"` + DisplayName string `validate:"required,gte=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:"-"` @@ -98,3 +98,18 @@ type UpdateUserPasswordReq struct { Password string `validate:"required,gte=8,lte=32" json:"password"` LoginUserID string `json:"-"` } + +// GetUserActivationReq get user activation +type GetUserActivationReq struct { + UserID string `validate:"required" form:"user_id"` +} + +// GetUserActivationResp get user activation +type GetUserActivationResp struct { + ActivationURL string `json:"activation_url"` +} + +// SendUserActivationReq send user activation +type SendUserActivationReq struct { + UserID string `validate:"required" json:"user_id"` +} diff --git a/internal/schema/comment_schema.go b/internal/schema/comment_schema.go index 4d64cf01..caba2cea 100644 --- a/internal/schema/comment_schema.go +++ b/internal/schema/comment_schema.go @@ -26,7 +26,9 @@ type AddCommentReq struct { // whether user can edit it CanEdit bool `json:"-"` // whether user can delete it - CanDelete bool `json:"-"` + CanDelete bool `json:"-"` + CaptchaID string `json:"captcha_id"` // captcha_id + CaptchaCode string `json:"captcha_code"` } func (req *AddCommentReq) Check() (errFields []*validator.FormErrorField, err error) { @@ -39,7 +41,9 @@ type RemoveCommentReq struct { // comment id CommentID string `validate:"required" json:"comment_id"` // user id - UserID string `json:"-"` + UserID string `json:"-"` + CaptchaID string `json:"captcha_id"` // captcha_id + CaptchaCode string `json:"captcha_code"` } // UpdateCommentReq update comment request @@ -58,7 +62,9 @@ type UpdateCommentReq struct { // whether user can edit it CanEdit bool `json:"-"` // whether user can delete it - CanDelete bool `json:"-"` + CanDelete bool `json:"-"` + CaptchaID string `json:"captcha_id"` // captcha_id + CaptchaCode string `json:"captcha_code"` } func (req *UpdateCommentReq) Check() (errFields []*validator.FormErrorField, err error) { diff --git a/internal/schema/dashboard_schema.go b/internal/schema/dashboard_schema.go index 88afed90..844023d4 100644 --- a/internal/schema/dashboard_schema.go +++ b/internal/schema/dashboard_schema.go @@ -5,8 +5,8 @@ import "time" var AppStartTime time.Time const ( - DashBoardCachekey = "answer@dashboard" - DashBoardCacheTime = 60 * time.Minute + DashboardCacheKey = "answer:dashboard" + DashboardCacheTime = 60 * time.Minute ) type DashboardInfo struct { diff --git a/internal/schema/notification_schema.go b/internal/schema/notification_schema.go index 4e617efd..7dc5f746 100644 --- a/internal/schema/notification_schema.go +++ b/internal/schema/notification_schema.go @@ -63,6 +63,8 @@ type NotificationMsg struct { NotificationAction string // if true no need to send notification to all followers NoNeedPushAllFollow bool + // extra info + ExtraInfo map[string]string } type ObjectInfo struct { diff --git a/internal/schema/question_schema.go b/internal/schema/question_schema.go index acabc0d2..6a586b19 100644 --- a/internal/schema/question_schema.go +++ b/internal/schema/question_schema.go @@ -1,16 +1,16 @@ package schema import ( + "strings" "time" "github.com/answerdev/answer/internal/base/validator" + "github.com/answerdev/answer/internal/entity" "github.com/answerdev/answer/pkg/converter" + "github.com/answerdev/answer/pkg/uid" ) const ( - SitemapMaxSize = 50000 - SitemapCachekey = "answer@sitemap" - SitemapPageCachekey = "answer@sitemap@page%d" QuestionOperationPin = "pin" QuestionOperationUnPin = "unpin" QuestionOperationHide = "hide" @@ -20,9 +20,11 @@ const ( // RemoveQuestionReq delete question request type RemoveQuestionReq struct { // question id - ID string `validate:"required" json:"id"` - UserID string `json:"-" ` // user_id - IsAdmin bool `json:"-"` + ID string `validate:"required" json:"id"` + UserID string `json:"-" ` // user_id + IsAdmin bool `json:"-"` + CaptchaID string `json:"captcha_id"` // captcha_id + CaptchaCode string `json:"captcha_code"` } type CloseQuestionReq struct { @@ -63,6 +65,8 @@ type QuestionAdd struct { // user id UserID string `json:"-"` QuestionPermission + CaptchaID string `json:"captcha_id"` // captcha_id + CaptchaCode string `json:"captcha_code"` } func (req *QuestionAdd) Check() (errFields []*validator.FormErrorField, err error) { @@ -90,6 +94,8 @@ type QuestionAddByAnswer struct { UserID string `json:"-"` MentionUsernameList []string `validate:"omitempty" json:"mention_username_list"` QuestionPermission + CaptchaID string `json:"captcha_id"` // captcha_id + CaptchaCode string `json:"captcha_code"` } func (req *QuestionAddByAnswer) Check() (errFields []*validator.FormErrorField, err error) { @@ -153,6 +159,8 @@ type QuestionUpdate struct { UserID string `json:"-"` NoNeedReview bool `json:"-"` QuestionPermission + CaptchaID string `json:"captcha_id"` // captcha_id + CaptchaCode string `json:"captcha_code"` } type QuestionUpdateInviteUser struct { @@ -160,6 +168,8 @@ type QuestionUpdateInviteUser struct { InviteUser []string `validate:"omitempty" json:"invite_user"` UserID string `json:"-"` QuestionPermission + CaptchaID string `json:"captcha_id"` // captcha_id + CaptchaCode string `json:"captcha_code"` } func (req *QuestionUpdate) Check() (errFields []*validator.FormErrorField, err error) { @@ -361,12 +371,62 @@ type QuestionPageRespOperator struct { DisplayName string `json:"display_name"` } -type AdminQuestionSearch struct { - Page int `json:"page" form:"page"` // Query number of pages - PageSize int `json:"page_size" form:"page_size"` // Search page size - Status int `json:"-" form:"-"` - StatusStr string `json:"status" form:"status"` // Status 1 Available 2 closed 10 UserDeleted - Query string `validate:"omitempty,gt=0,lte=100" json:"query" form:"query" ` //Query string +type AdminQuestionPageReq struct { + Page int `validate:"omitempty,min=1" form:"page"` + PageSize int `validate:"omitempty,min=1" form:"page_size"` + StatusCond string `validate:"omitempty,oneof=normal closed deleted" form:"status"` + Query string `validate:"omitempty,gt=0,lte=100" json:"query" form:"query" ` + Status int `json:"-"` + LoginUserID string `json:"-"` +} + +func (req *AdminQuestionPageReq) Check() (errField []*validator.FormErrorField, err error) { + status, ok := entity.AdminQuestionSearchStatus[req.StatusCond] + if ok { + req.Status = status + } + if req.Status == 0 { + req.Status = 1 + } + return nil, nil +} + +// AdminAnswerPageReq admin answer page req +type AdminAnswerPageReq struct { + Page int `validate:"omitempty,min=1" form:"page"` + PageSize int `validate:"omitempty,min=1" form:"page_size"` + StatusCond string `validate:"omitempty,oneof=normal deleted" form:"status"` + Query string `validate:"omitempty,gt=0,lte=100" form:"query"` + QuestionID string `validate:"omitempty,gt=0,lte=24" form:"question_id"` + QuestionTitle string `json:"-"` + AnswerID string `json:"-"` + Status int `json:"-"` + LoginUserID string `json:"-"` +} + +func (req *AdminAnswerPageReq) Check() (errField []*validator.FormErrorField, err error) { + req.QuestionID = uid.DeShortID(req.QuestionID) + if req.QuestionID == "0" { + req.QuestionID = "" + } + + if status, ok := entity.AdminAnswerSearchStatus[req.StatusCond]; ok { + req.Status = status + } + if req.Status == 0 { + req.Status = 1 + } + + // parse query condition + if len(req.Query) > 0 { + prefix := "answer:" + if strings.Contains(req.Query, prefix) { + req.AnswerID = uid.DeShortID(strings.TrimSpace(strings.TrimPrefix(req.Query, prefix))) + } else { + req.QuestionTitle = strings.TrimSpace(req.Query) + } + } + return nil, nil } type AdminSetQuestionStatusRequest struct { @@ -374,21 +434,6 @@ type AdminSetQuestionStatusRequest struct { 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"` -} - type PersonalQuestionPageReq struct { Page int `validate:"omitempty,min=1" form:"page"` PageSize int `validate:"omitempty,min=1" form:"page_size"` diff --git a/internal/schema/report_schema.go b/internal/schema/report_schema.go index b4534f82..579aba75 100644 --- a/internal/schema/report_schema.go +++ b/internal/schema/report_schema.go @@ -15,7 +15,9 @@ type AddReportReq struct { // report content Content string `validate:"omitempty,gt=0,lte=500" json:"content"` // user id - UserID string `json:"-"` + UserID string `json:"-"` + CaptchaID string `json:"captcha_id"` // captcha_id + CaptchaCode string `json:"captcha_code"` } // GetReportListReq get report list all request diff --git a/internal/schema/search_schema.go b/internal/schema/search_schema.go index 51ec0f6a..94b237a0 100644 --- a/internal/schema/search_schema.go +++ b/internal/schema/search_schema.go @@ -1,11 +1,83 @@ package schema +import ( + "github.com/answerdev/answer/internal/base/constant" + "github.com/answerdev/answer/plugin" +) + type SearchDTO struct { - UserID string // UserID current login user ID - Query string `validate:"required,gte=1,lte=60" json:"q" form:"q"` // Query the query string - Page int `validate:"omitempty,min=1" form:"page,default=1" json:"page"` //Query number of pages - Size int `validate:"omitempty,min=1,max=50" form:"size,default=30" json:"size"` //Search page size - Order string `validate:"required,oneof=newest active score relevance" form:"order,default=relevance" json:"order" enums:"newest,active,score,relevance"` + UserID string // UserID current login user ID + Query string `validate:"required,gte=1,lte=60" json:"q" form:"q"` // Query the query string + Page int `validate:"omitempty,min=1" form:"page,default=1" json:"page"` //Query number of pages + Size int `validate:"omitempty,min=1,max=50" form:"size,default=30" json:"size"` //Search page size + Order string `validate:"required,oneof=newest active score relevance" form:"order,default=relevance" json:"order" enums:"newest,active,score,relevance"` + CaptchaID string `json:"captcha_id"` // captcha_id + CaptchaCode string `json:"captcha_code"` +} + +type SearchCondition struct { + // search target type: all/question/answer + TargetType string + // search query user id + UserID string + // vote amount + VoteAmount int + // only show not accepted answer's question + NotAccepted bool + // view amount + Views int + // answer count + AnswerAmount int + // only show accepted answer + Accepted bool + // only show this question's answer + QuestionID string + // search query tags + Tags []string + // search query keywords + Words []string +} + +// SearchAll check if search all +func (s *SearchCondition) SearchAll() bool { + return len(s.TargetType) == 0 +} + +// SearchQuestion check if search only need question +func (s *SearchCondition) SearchQuestion() bool { + return s.TargetType == constant.QuestionObjectType +} + +// SearchAnswer check if search only need answer +func (s *SearchCondition) SearchAnswer() bool { + return s.TargetType == constant.AnswerObjectType +} + +// Convert2PluginSearchCond convert to plugin search condition +func (s *SearchCondition) Convert2PluginSearchCond(page, pageSize int, order string) *plugin.SearchBasicCond { + basic := &plugin.SearchBasicCond{ + Page: page, + PageSize: pageSize, + Words: s.Words, + TagIDs: s.Tags, + UserID: s.UserID, + Order: plugin.SearchOrderCond(order), + QuestionID: s.QuestionID, + VoteAmount: s.VoteAmount, + ViewAmount: s.Views, + AnswerAmount: s.AnswerAmount, + } + if s.Accepted { + basic.AnswerAccepted = plugin.AcceptedCondTrue + } else { + basic.AnswerAccepted = plugin.AcceptedCondAll + } + if s.NotAccepted { + basic.QuestionAccepted = plugin.AcceptedCondFalse + } else { + basic.QuestionAccepted = plugin.AcceptedCondAll + } + return basic } type SearchObject struct { diff --git a/internal/schema/siteinfo_schema.go b/internal/schema/siteinfo_schema.go index 119420cf..e21e8ff4 100644 --- a/internal/schema/siteinfo_schema.go +++ b/internal/schema/siteinfo_schema.go @@ -14,11 +14,6 @@ import ( "github.com/segmentfault/pacman/errors" ) -const PermaLinkQuestionIDAndTitle = 1 // /questions/10010000000000001/post-title -const PermaLinkQuestionID = 2 // /questions/10010000000000001 -const PermaLinkQuestionIDAndTitleByShortID = 3 // /questions/11/post-title -const PermaLinkQuestionIDByShortID = 4 // /questions/11 - // SiteGeneralReq site general request type SiteGeneralReq struct { Name string `validate:"required,sanitizer,gt=1,lte=128" form:"name" json:"name"` @@ -28,11 +23,6 @@ type SiteGeneralReq struct { ContactEmail string `validate:"required,sanitizer,gt=1,lte=512,email" form:"contact_email" json:"contact_email"` } -type SiteSeoReq struct { - PermaLink int `validate:"required,lte=4,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 { @@ -127,6 +117,16 @@ type SiteThemeReq struct { ThemeConfig map[string]interface{} `validate:"omitempty" json:"theme_config"` } +type SiteSeoReq struct { + PermaLink int `validate:"required,lte=4,gte=0" form:"permalink" json:"permalink"` + Robots string `validate:"required" form:"robots" json:"robots"` +} + +func (s *SiteSeoResp) IsShortLink() bool { + return s.PermaLink == constant.PermaLinkQuestionIDAndTitleByShortID || + s.PermaLink == constant.PermaLinkQuestionIDByShortID +} + // SiteGeneralResp site general response type SiteGeneralResp SiteGeneralReq @@ -186,7 +186,7 @@ type SiteInfoResp struct { Login *SiteLoginResp `json:"login"` Theme *SiteThemeResp `json:"theme"` CustomCssHtml *SiteCustomCssHTMLResp `json:"custom_css_html"` - SiteSeo *SiteSeoReq `json:"site_seo"` + SiteSeo *SiteSeoResp `json:"site_seo"` SiteUsers *SiteUsersResp `json:"site_users"` Version string `json:"version"` Revision string `json:"revision"` @@ -195,7 +195,7 @@ type TemplateSiteInfoResp struct { General *SiteGeneralResp `json:"general"` Interface *SiteInterfaceResp `json:"interface"` Branding *SiteBrandingResp `json:"branding"` - SiteSeo *SiteSeoReq `json:"site_seo"` + SiteSeo *SiteSeoResp `json:"site_seo"` CustomCssHtml *SiteCustomCssHTMLResp `json:"custom_css_html"` Title string Year string @@ -265,6 +265,16 @@ const ( ) type PrivilegeLevel int +type PrivilegeOptions []*PrivilegeOption + +func (p PrivilegeOptions) Choose(level PrivilegeLevel) (option *PrivilegeOption) { + for _, op := range p { + if op.Level == level { + return op + } + } + return nil +} // GetPrivilegesConfigResp get privileges config response type GetPrivilegesConfigResp struct { @@ -285,7 +295,7 @@ type UpdatePrivilegesConfigReq struct { } var ( - DefaultPrivilegeOptions []*PrivilegeOption + DefaultPrivilegeOptions PrivilegeOptions privilegeOptionsLevelMapping = map[string][]int{ constant.RankQuestionAddKey: {1, 1, 1}, constant.RankAnswerAddKey: {1, 1, 1}, @@ -293,8 +303,8 @@ var ( constant.RankReportAddKey: {1, 1, 1}, constant.RankCommentVoteUpKey: {1, 1, 1}, constant.RankLinkUrlLimitKey: {1, 10, 10}, - constant.RankQuestionVoteUpKey: {1, 1, 15}, - constant.RankAnswerVoteUpKey: {1, 1, 15}, + constant.RankQuestionVoteUpKey: {1, 8, 15}, + constant.RankAnswerVoteUpKey: {1, 8, 15}, constant.RankQuestionVoteDownKey: {125, 125, 125}, constant.RankAnswerVoteDownKey: {125, 125, 125}, constant.RankInviteSomeoneToAnswerKey: {1, 500, 1000}, diff --git a/internal/schema/sitemap_schema.go b/internal/schema/sitemap_schema.go new file mode 100644 index 00000000..08e27f8d --- /dev/null +++ b/internal/schema/sitemap_schema.go @@ -0,0 +1,16 @@ +package schema + +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/user_schema.go b/internal/schema/user_schema.go index 38256b98..1489ae39 100644 --- a/internal/schema/user_schema.go +++ b/internal/schema/user_schema.go @@ -164,11 +164,6 @@ const ( NoticeStatusOn = 1 NoticeStatusOff = 2 - - ActionRecordTypeLogin = "login" - ActionRecordTypeEmail = "e_mail" - ActionRecordTypeFindPass = "find_pass" - ActionRecordTypeModifyPass = "modify_pass" ) var UserStatusShow = map[int]string{ @@ -331,8 +326,9 @@ type UserNoticeSetResp struct { type ActionRecordReq struct { // action - Action string `validate:"required,oneof=login e_mail find_pass modify_pass" form:"action"` + Action string `validate:"required,oneof=email password edit_userinfo question answer comment edit invitation_answer search report delete vote" form:"action"` IP string `json:"-"` + UserID string `json:"-"` } type ActionRecordResp struct { diff --git a/internal/schema/vote_schema.go b/internal/schema/vote_schema.go index 4cb86c66..b9d17acd 100644 --- a/internal/schema/vote_schema.go +++ b/internal/schema/vote_schema.go @@ -1,50 +1,60 @@ package schema type VoteReq struct { - ObjectID string `validate:"required" form:"object_id" json:"object_id"` // id - IsCancel bool `validate:"omitempty" form:"is_cancel" json:"is_cancel"` // is cancel - UserID string `json:"-"` -} - -type VoteDTO struct { - // object TagID - ObjectID string - // is cancel - IsCancel bool - // user TagID - UserID string + ObjectID string `validate:"required" form:"object_id" json:"object_id"` // id + IsCancel bool `validate:"omitempty" form:"is_cancel" json:"is_cancel"` // is cancel + UserID string `json:"-"` + CaptchaID string `json:"captcha_id"` // captcha_id + CaptchaCode string `json:"captcha_code"` } type VoteResp struct { - UpVotes int `json:"up_votes"` - DownVotes int `json:"down_votes"` - Votes int `json:"votes"` + UpVotes int64 `json:"up_votes"` + DownVotes int64 `json:"down_votes"` + Votes int64 `json:"votes"` VoteStatus string `json:"vote_status"` } +// VoteOperationInfo vote operation info +type VoteOperationInfo struct { + // operation object id + ObjectID string + // question answer comment + ObjectType string + // object owner user id + ObjectCreatorUserID string + // operation user id + OperatingUserID string + // vote up + VoteUp bool + // vote down + VoteDown bool + // vote activity info + Activities []*VoteActivity +} + +// VoteActivity vote activity +type VoteActivity struct { + ActivityType int + ActivityUserID string + TriggerUserID string + Rank int +} + +func (v *VoteActivity) HasRank() int { + if v.Rank != 0 { + return 1 + } + return 0 +} + type GetVoteWithPageReq struct { // page Page int `validate:"omitempty,min=1" form:"page"` // page size PageSize int `validate:"omitempty,min=1" form:"page_size"` // user id - UserID string `validate:"required" form:"user_id"` -} - -type VoteQuestion struct { - // object ID - ID string `json:"id"` - // title - Title string `json:"title"` -} - -type VoteAnswer struct { - // object ID - ID string `json:"id"` - // question ID - QuestionID string `json:"question_id"` - // title - Title string `json:"title"` + UserID string `json:"-"` } type GetVoteWithPageResp struct { diff --git a/internal/service/action/captcha_service.go b/internal/service/action/captcha_service.go index 84a189ed..b3cdf087 100644 --- a/internal/service/action/captcha_service.go +++ b/internal/service/action/captcha_service.go @@ -6,6 +6,7 @@ import ( "strings" "github.com/answerdev/answer/internal/base/reason" + "github.com/answerdev/answer/internal/entity" "github.com/answerdev/answer/internal/schema" "github.com/mojocn/base64Captcha" "github.com/segmentfault/pacman/errors" @@ -17,9 +18,9 @@ type CaptchaRepo interface { SetCaptcha(ctx context.Context, key, captcha string) (err error) GetCaptcha(ctx context.Context, key string) (captcha string, err error) DelCaptcha(ctx context.Context, key string) (err error) - SetActionType(ctx context.Context, ip, actionType string, amount int) (err error) - GetActionType(ctx context.Context, ip, actionType string) (amount int, err error) - DelActionType(ctx context.Context, ip, actionType string) (err error) + SetActionType(ctx context.Context, unit, actionType, config string, amount int) (err error) + GetActionType(ctx context.Context, unit, actionType string) (actioninfo *entity.ActionRecordInfo, err error) + DelActionType(ctx context.Context, unit, actionType string) (err error) } // CaptchaService kit service @@ -37,12 +38,33 @@ func NewCaptchaService(captchaRepo CaptchaRepo) *CaptchaService { // ActionRecord action record func (cs *CaptchaService) ActionRecord(ctx context.Context, req *schema.ActionRecordReq) (resp *schema.ActionRecordResp, err error) { resp = &schema.ActionRecordResp{} - num, err := cs.captchaRepo.GetActionType(ctx, req.IP, req.Action) - if err != nil { - num = 0 + unit := req.IP + switch req.Action { + case entity.CaptchaActionEditUserinfo: + unit = req.UserID + case entity.CaptchaActionQuestion: + unit = req.UserID + case entity.CaptchaActionAnswer: + unit = req.UserID + case entity.CaptchaActionComment: + unit = req.UserID + case entity.CaptchaActionEdit: + unit = req.UserID + case entity.CaptchaActionInvitationAnswer: + unit = req.UserID + case entity.CaptchaActionSearch: + if req.UserID != "" { + unit = req.UserID + } + case entity.CaptchaActionReport: + unit = req.UserID + case entity.CaptchaActionDelete: + unit = req.UserID + case entity.CaptchaActionVote: + unit = req.UserID } - // TODO config num to config file - if num >= 3 { + verificationResult := cs.ValidationStrategy(ctx, unit, req.Action) + if !verificationResult { resp.CaptchaID, resp.CaptchaImg, err = cs.GenerateCaptcha(ctx) resp.Verify = true } @@ -72,13 +94,10 @@ func (cs *CaptchaService) UserRegisterVerifyCaptcha( // ActionRecordVerifyCaptcha // Verify that you need to enter a CAPTCHA, and that the CAPTCHA is correct func (cs *CaptchaService) ActionRecordVerifyCaptcha( - ctx context.Context, actionType string, ip string, id string, VerifyValue string, + ctx context.Context, actionType string, unit string, id string, VerifyValue string, ) bool { - num, cahceErr := cs.captchaRepo.GetActionType(ctx, ip, actionType) - if cahceErr != nil { - return true - } - if num >= 3 { + verificationResult := cs.ValidationStrategy(ctx, unit, actionType) + if !verificationResult { if id == "" || VerifyValue == "" { return false } @@ -91,22 +110,22 @@ func (cs *CaptchaService) ActionRecordVerifyCaptcha( return true } -func (cs *CaptchaService) ActionRecordAdd(ctx context.Context, actionType string, ip string) (int, error) { +func (cs *CaptchaService) ActionRecordAdd(ctx context.Context, actionType string, unit string) (int, error) { var err error - num, cahceErr := cs.captchaRepo.GetActionType(ctx, ip, actionType) + info, cahceErr := cs.captchaRepo.GetActionType(ctx, unit, actionType) if cahceErr != nil { log.Error(err) } - num++ - err = cs.captchaRepo.SetActionType(ctx, ip, actionType, num) + info.Num++ + err = cs.captchaRepo.SetActionType(ctx, unit, actionType, "", info.Num) if err != nil { return 0, err } - return num, nil + return info.Num, nil } -func (cs *CaptchaService) ActionRecordDel(ctx context.Context, actionType string, ip string) { - err := cs.captchaRepo.DelActionType(ctx, ip, actionType) +func (cs *CaptchaService) ActionRecordDel(ctx context.Context, actionType string, unit string) { + err := cs.captchaRepo.DelActionType(ctx, unit, actionType) if err != nil { log.Error(err) } @@ -115,13 +134,13 @@ func (cs *CaptchaService) ActionRecordDel(ctx context.Context, actionType string // GenerateCaptcha generate captcha func (cs *CaptchaService) GenerateCaptcha(ctx context.Context) (key, captchaBase64 string, err error) { driverString := base64Captcha.DriverString{ - Height: 40, - Width: 100, + Height: 60, + Width: 200, NoiseCount: 0, ShowLineOptions: 2 | 4, Length: 4, Source: "1234567890qwertyuioplkjhgfdsazxcvbnm", - BgColor: &color.RGBA{R: 3, G: 102, B: 214, A: 125}, + BgColor: &color.RGBA{R: 211, G: 211, B: 211, A: 0}, Fonts: []string{"wqy-microhei.ttc"}, } driver := driverString.ConvertFonts() diff --git a/internal/service/action/captcha_strategy.go b/internal/service/action/captcha_strategy.go new file mode 100644 index 00000000..b5187814 --- /dev/null +++ b/internal/service/action/captcha_strategy.go @@ -0,0 +1,166 @@ +package action + +import ( + "context" + "time" + + "github.com/answerdev/answer/internal/entity" +) + +// ValidationStrategy +// true pass +// false need captcha +func (cs *CaptchaService) ValidationStrategy(ctx context.Context, unit, actionType string) bool { + info, err := cs.captchaRepo.GetActionType(ctx, unit, actionType) + if err != nil { + //No record, no processing + // + } + switch actionType { + case entity.CaptchaActionEmail: + return cs.CaptchaActionEmail(ctx, unit, info) + case entity.CaptchaActionPassword: + return cs.CaptchaActionPassword(ctx, unit, info) + case entity.CaptchaActionEditUserinfo: + return cs.CaptchaActionEditUserinfo(ctx, unit, info) + case entity.CaptchaActionQuestion: + return cs.CaptchaActionQuestion(ctx, unit, info) + case entity.CaptchaActionAnswer: + return cs.CaptchaActionAnswer(ctx, unit, info) + case entity.CaptchaActionComment: + return cs.CaptchaActionComment(ctx, unit, info) + case entity.CaptchaActionEdit: + return cs.CaptchaActionEdit(ctx, unit, info) + case entity.CaptchaActionInvitationAnswer: + return cs.CaptchaActionInvitationAnswer(ctx, unit, info) + case entity.CaptchaActionSearch: + return cs.CaptchaActionSearch(ctx, unit, info) + case entity.CaptchaActionReport: + return cs.CaptchaActionReport(ctx, unit, info) + case entity.CaptchaActionDelete: + return cs.CaptchaActionDelete(ctx, unit, info) + case entity.CaptchaActionVote: + return cs.CaptchaActionVote(ctx, unit, info) + + } + //actionType not found + return false +} + +func (cs *CaptchaService) CaptchaActionEmail(ctx context.Context, unit string, actioninfo *entity.ActionRecordInfo) bool { + // You need a verification code every time + return false +} + +func (cs *CaptchaService) CaptchaActionPassword(ctx context.Context, unit string, actioninfo *entity.ActionRecordInfo) bool { + setNum := 3 + setTime := int64(60 * 30) //seconds + now := time.Now().Unix() + if now-actioninfo.LastTime <= setTime || actioninfo.Num >= setNum { + return false + } + if now-actioninfo.LastTime > setTime { + cs.captchaRepo.SetActionType(ctx, unit, entity.CaptchaActionPassword, "", 0) + } + return true +} + +func (cs *CaptchaService) CaptchaActionEditUserinfo(ctx context.Context, unit string, actioninfo *entity.ActionRecordInfo) bool { + setNum := 3 + setTime := int64(60 * 30) //seconds + now := time.Now().Unix() + if now-actioninfo.LastTime <= setTime || actioninfo.Num >= setNum { + return false + } + if now-actioninfo.LastTime > setTime { + cs.captchaRepo.SetActionType(ctx, unit, entity.CaptchaActionEditUserinfo, "", 0) + } + return true +} + +func (cs *CaptchaService) CaptchaActionQuestion(ctx context.Context, unit string, actioninfo *entity.ActionRecordInfo) bool { + setNum := 10 + setTime := int64(5) //seconds + now := time.Now().Unix() + if now-actioninfo.LastTime <= setTime || actioninfo.Num >= setNum { + return false + } + return true +} + +func (cs *CaptchaService) CaptchaActionAnswer(ctx context.Context, unit string, actioninfo *entity.ActionRecordInfo) bool { + setNum := 10 + setTime := int64(5) //seconds + now := time.Now().Unix() + if now-actioninfo.LastTime <= setTime || actioninfo.Num >= setNum { + return false + } + return true +} + +func (cs *CaptchaService) CaptchaActionComment(ctx context.Context, unit string, actioninfo *entity.ActionRecordInfo) bool { + setNum := 30 + setTime := int64(1) //seconds + now := time.Now().Unix() + if now-actioninfo.LastTime <= setTime || actioninfo.Num >= setNum { + return false + } + return true +} + +func (cs *CaptchaService) CaptchaActionEdit(ctx context.Context, unit string, actioninfo *entity.ActionRecordInfo) bool { + setNum := 10 + if actioninfo.Num >= setNum { + return false + } + return true +} + +func (cs *CaptchaService) CaptchaActionInvitationAnswer(ctx context.Context, unit string, actioninfo *entity.ActionRecordInfo) bool { + setNum := 30 + if actioninfo.Num >= setNum { + return false + } + return true +} + +func (cs *CaptchaService) CaptchaActionSearch(ctx context.Context, unit string, actioninfo *entity.ActionRecordInfo) bool { + now := time.Now().Unix() + setNum := 20 + setTime := int64(60) //seconds + if now-int64(actioninfo.LastTime) <= setTime && actioninfo.Num >= setNum { + return false + } + if now-actioninfo.LastTime > setTime { + cs.captchaRepo.SetActionType(ctx, unit, entity.CaptchaActionSearch, "", 0) + } + return true +} + +func (cs *CaptchaService) CaptchaActionReport(ctx context.Context, unit string, actioninfo *entity.ActionRecordInfo) bool { + setNum := 30 + setTime := int64(1) //seconds + now := time.Now().Unix() + if now-actioninfo.LastTime <= setTime || actioninfo.Num >= setNum { + return false + } + return true +} + +func (cs *CaptchaService) CaptchaActionDelete(ctx context.Context, unit string, actioninfo *entity.ActionRecordInfo) bool { + setNum := 5 + setTime := int64(5) //seconds + now := time.Now().Unix() + if now-actioninfo.LastTime <= setTime || actioninfo.Num >= setNum { + return false + } + return true +} + +func (cs *CaptchaService) CaptchaActionVote(ctx context.Context, unit string, actioninfo *entity.ActionRecordInfo) bool { + setNum := 40 + if actioninfo.Num >= setNum { + return false + } + return true +} diff --git a/internal/service/activity/activity.go b/internal/service/activity/activity.go index ebd5e6df..357de2a0 100644 --- a/internal/service/activity/activity.go +++ b/internal/service/activity/activity.go @@ -4,12 +4,13 @@ import ( "context" "encoding/json" "fmt" + "github.com/answerdev/answer/internal/service/activity_common" "strings" "github.com/answerdev/answer/internal/base/constant" + "github.com/answerdev/answer/internal/base/handler" "github.com/answerdev/answer/internal/entity" "github.com/answerdev/answer/internal/schema" - "github.com/answerdev/answer/internal/service/activity_common" "github.com/answerdev/answer/internal/service/comment_common" "github.com/answerdev/answer/internal/service/config" "github.com/answerdev/answer/internal/service/meta" @@ -97,7 +98,9 @@ func (as *ActivityService) GetObjectTimeline(ctx context.Context, req *schema.Ge } if item.ObjectType == constant.QuestionObjectType || item.ObjectType == constant.AnswerObjectType { - item.ObjectID = uid.EnShortID(act.ObjectID) + if handler.GetEnableShortID(ctx) { + item.ObjectID = uid.EnShortID(act.ObjectID) + } } cfg, err := as.configService.GetConfigByID(ctx, act.ActivityType) diff --git a/internal/service/activity/answer_activity.go b/internal/service/activity/answer_activity.go deleted file mode 100644 index 2a8e51d8..00000000 --- a/internal/service/activity/answer_activity.go +++ /dev/null @@ -1,77 +0,0 @@ -package activity - -import ( - "context" - "time" - - "github.com/segmentfault/pacman/log" -) - -// AnswerActivityRepo answer activity -type AnswerActivityRepo interface { - AcceptAnswer(ctx context.Context, - answerObjID, questionObjID, questionUserID, answerUserID string, isSelf bool) (err error) - CancelAcceptAnswer(ctx context.Context, - answerObjID, questionObjID, questionUserID, answerUserID string) (err error) - DeleteAnswer(ctx context.Context, answerID string) (err error) -} - -// QuestionActivityRepo answer activity -type QuestionActivityRepo interface { - DeleteQuestion(ctx context.Context, questionID string) (err error) -} - -// AnswerActivityService user service -type AnswerActivityService struct { - answerActivityRepo AnswerActivityRepo - questionActivityRepo QuestionActivityRepo -} - -// NewAnswerActivityService new comment service -func NewAnswerActivityService( - answerActivityRepo AnswerActivityRepo, questionActivityRepo QuestionActivityRepo) *AnswerActivityService { - return &AnswerActivityService{ - answerActivityRepo: answerActivityRepo, - questionActivityRepo: questionActivityRepo, - } -} - -// AcceptAnswer accept answer change activity -func (as *AnswerActivityService) AcceptAnswer(ctx context.Context, - answerObjID, questionObjID, questionUserID, answerUserID string, isSelf bool) (err error) { - return as.answerActivityRepo.AcceptAnswer(ctx, answerObjID, questionObjID, questionUserID, answerUserID, isSelf) -} - -// CancelAcceptAnswer cancel accept answer change activity -func (as *AnswerActivityService) CancelAcceptAnswer(ctx context.Context, - answerObjID, questionObjID, questionUserID, answerUserID string) (err error) { - return as.answerActivityRepo.CancelAcceptAnswer(ctx, answerObjID, questionObjID, questionUserID, answerUserID) -} - -// DeleteAnswer delete answer change activity -func (as *AnswerActivityService) DeleteAnswer(ctx context.Context, answerID string, createdAt time.Time, - voteCount int) (err error) { - if voteCount >= 3 { - log.Infof("There is no need to roll back the reputation by answering likes above the target value. %s %d", answerID, voteCount) - return nil - } - if createdAt.Before(time.Now().AddDate(0, 0, -60)) { - log.Infof("There is no need to roll back the reputation by answer's existence time meets the target. %s %s", answerID, createdAt.String()) - return nil - } - return as.answerActivityRepo.DeleteAnswer(ctx, answerID) -} - -// DeleteQuestion delete question change activity -func (as *AnswerActivityService) DeleteQuestion(ctx context.Context, questionID string, createdAt time.Time, - voteCount int) (err error) { - if voteCount >= 3 { - log.Infof("There is no need to roll back the reputation by answering likes above the target value. %s %d", questionID, voteCount) - return nil - } - if createdAt.Before(time.Now().AddDate(0, 0, -60)) { - log.Infof("There is no need to roll back the reputation by answer's existence time meets the target. %s %s", questionID, createdAt.String()) - return nil - } - return as.questionActivityRepo.DeleteQuestion(ctx, questionID) -} diff --git a/internal/service/activity/answer_activity_service.go b/internal/service/activity/answer_activity_service.go new file mode 100644 index 00000000..daab2bf7 --- /dev/null +++ b/internal/service/activity/answer_activity_service.go @@ -0,0 +1,93 @@ +package activity + +import ( + "context" + "github.com/answerdev/answer/internal/schema" + "github.com/answerdev/answer/internal/service/activity_type" + "github.com/answerdev/answer/internal/service/config" + "github.com/segmentfault/pacman/log" +) + +// AnswerActivityRepo answer activity +type AnswerActivityRepo interface { + SaveAcceptAnswerActivity(ctx context.Context, op *schema.AcceptAnswerOperationInfo) (err error) + SaveCancelAcceptAnswerActivity(ctx context.Context, op *schema.AcceptAnswerOperationInfo) (err error) +} + +// AnswerActivityService answer activity service +type AnswerActivityService struct { + answerActivityRepo AnswerActivityRepo + configService *config.ConfigService +} + +// NewAnswerActivityService new comment service +func NewAnswerActivityService( + answerActivityRepo AnswerActivityRepo, + configService *config.ConfigService, +) *AnswerActivityService { + return &AnswerActivityService{ + answerActivityRepo: answerActivityRepo, + configService: configService, + } +} + +// AcceptAnswer accept answer change activity +func (as *AnswerActivityService) AcceptAnswer(ctx context.Context, + loginUserID, answerObjID, questionObjID, questionUserID, answerUserID string, isSelf bool) (err error) { + operationInfo := as.createAcceptAnswerOperationInfo(ctx, loginUserID, + answerObjID, questionObjID, questionUserID, answerUserID, isSelf) + return as.answerActivityRepo.SaveAcceptAnswerActivity(ctx, operationInfo) +} + +// CancelAcceptAnswer cancel accept answer change activity +func (as *AnswerActivityService) CancelAcceptAnswer(ctx context.Context, + loginUserID, answerObjID, questionObjID, questionUserID, answerUserID string) (err error) { + operationInfo := as.createAcceptAnswerOperationInfo(ctx, loginUserID, + answerObjID, questionObjID, questionUserID, answerUserID, false) + return as.answerActivityRepo.SaveCancelAcceptAnswerActivity(ctx, operationInfo) +} + +func (as *AnswerActivityService) createAcceptAnswerOperationInfo(ctx context.Context, loginUserID, + answerObjID, questionObjID, questionUserID, answerUserID string, isSelf bool) *schema.AcceptAnswerOperationInfo { + operationInfo := &schema.AcceptAnswerOperationInfo{ + TriggerUserID: loginUserID, + QuestionObjectID: questionObjID, + QuestionUserID: questionUserID, + AnswerObjectID: answerObjID, + AnswerUserID: answerUserID, + } + operationInfo.Activities = as.getActivities(ctx, operationInfo) + if isSelf { + for _, activity := range operationInfo.Activities { + activity.Rank = 0 + } + } + return operationInfo +} + +func (as *AnswerActivityService) getActivities(ctx context.Context, op *schema.AcceptAnswerOperationInfo) ( + activities []*schema.AcceptAnswerActivity) { + activities = make([]*schema.AcceptAnswerActivity, 0) + + for _, action := range []string{activity_type.AnswerAccept, activity_type.AnswerAccepted} { + t := &schema.AcceptAnswerActivity{} + cfg, err := as.configService.GetConfigByKey(ctx, action) + if err != nil { + log.Warnf("get config by key error: %v", err) + continue + } + t.ActivityType, t.Rank = cfg.ID, cfg.GetIntValue() + + if action == activity_type.AnswerAccept { + t.ActivityUserID = op.QuestionUserID + t.TriggerUserID = op.TriggerUserID + t.OriginalObjectID = op.QuestionObjectID // if activity is 'accept' means this question is accept the answer. + } else { + t.ActivityUserID = op.AnswerUserID + t.TriggerUserID = op.TriggerUserID + t.OriginalObjectID = op.AnswerObjectID // if activity is 'accepted' means this answer was accepted. + } + activities = append(activities, t) + } + return activities +} diff --git a/internal/service/activity_common/activity.go b/internal/service/activity_common/activity.go index cf5b78b5..adfb55ad 100644 --- a/internal/service/activity_common/activity.go +++ b/internal/service/activity_common/activity.go @@ -5,6 +5,7 @@ import ( "time" "github.com/answerdev/answer/internal/entity" + "github.com/answerdev/answer/internal/schema" "github.com/answerdev/answer/internal/service/activity_queue" "github.com/answerdev/answer/pkg/converter" "github.com/answerdev/answer/pkg/uid" @@ -14,7 +15,7 @@ import ( type ActivityRepo interface { GetActivityTypeByObjID(ctx context.Context, objectId string, action string) (activityType, rank int, hasRank int, err error) - GetActivityTypeByObjKey(ctx context.Context, objectKey, action string) (activityType int, err error) + GetActivityTypeByObjectType(ctx context.Context, objectKey, action string) (activityType int, err error) GetActivity(ctx context.Context, session *xorm.Session, objectID, userID string, activityType int) ( existsActivity *entity.Activity, exist bool, err error) GetUserIDObjectIDActivitySum(ctx context.Context, userID, objectID string) (int, error) @@ -27,51 +28,44 @@ type ActivityRepo interface { } type ActivityCommon struct { - activityRepo ActivityRepo + activityRepo ActivityRepo + activityQueueService activity_queue.ActivityQueueService } // NewActivityCommon new activity common func NewActivityCommon( activityRepo ActivityRepo, + activityQueueService activity_queue.ActivityQueueService, ) *ActivityCommon { activity := &ActivityCommon{ - activityRepo: activityRepo, + activityRepo: activityRepo, + activityQueueService: activityQueueService, } - activity.HandleActivity() + activity.activityQueueService.RegisterHandler(activity.HandleActivity) return activity } // HandleActivity handle activity message -func (ac *ActivityCommon) HandleActivity() { - go func() { - defer func() { - if err := recover(); err != nil { - log.Error(err) - } - }() +func (ac *ActivityCommon) HandleActivity(ctx context.Context, msg *schema.ActivityMsg) error { + activityType, err := ac.activityRepo.GetActivityTypeByConfigKey(ctx, string(msg.ActivityTypeKey)) + if err != nil { + log.Errorf("error getting activity type %s, activity type is %d", err, activityType) + return err + } - for msg := range activity_queue.ActivityQueue { - log.Debugf("received activity %+v", msg) - - activityType, err := ac.activityRepo.GetActivityTypeByConfigKey(context.Background(), string(msg.ActivityTypeKey)) - if err != nil { - log.Errorf("error getting activity type %s, activity type is %d", err, activityType) - } - - act := &entity.Activity{ - UserID: msg.UserID, - TriggerUserID: msg.TriggerUserID, - ObjectID: uid.DeShortID(msg.ObjectID), - OriginalObjectID: uid.DeShortID(msg.OriginalObjectID), - ActivityType: activityType, - Cancelled: entity.ActivityAvailable, - } - if len(msg.RevisionID) > 0 { - act.RevisionID = converter.StringToInt64(msg.RevisionID) - } - if err := ac.activityRepo.AddActivity(context.TODO(), act); err != nil { - log.Error(err) - } - } - }() + act := &entity.Activity{ + UserID: msg.UserID, + TriggerUserID: msg.TriggerUserID, + ObjectID: uid.DeShortID(msg.ObjectID), + OriginalObjectID: uid.DeShortID(msg.OriginalObjectID), + ActivityType: activityType, + Cancelled: entity.ActivityAvailable, + } + if len(msg.RevisionID) > 0 { + act.RevisionID = converter.StringToInt64(msg.RevisionID) + } + if err := ac.activityRepo.AddActivity(ctx, act); err != nil { + return err + } + return nil } diff --git a/internal/service/activity_queue/activity_queue.go b/internal/service/activity_queue/activity_queue.go index 27897268..3561e3f5 100644 --- a/internal/service/activity_queue/activity_queue.go +++ b/internal/service/activity_queue/activity_queue.go @@ -1,14 +1,50 @@ package activity_queue import ( + "context" + "github.com/answerdev/answer/internal/schema" + "github.com/segmentfault/pacman/log" ) -var ( - ActivityQueue = make(chan *schema.ActivityMsg, 128) -) - -// AddActivity add new activity -func AddActivity(msg *schema.ActivityMsg) { - ActivityQueue <- msg +type ActivityQueueService interface { + Send(ctx context.Context, msg *schema.ActivityMsg) + RegisterHandler(handler func(ctx context.Context, msg *schema.ActivityMsg) error) +} + +type activityQueueService struct { + Queue chan *schema.ActivityMsg + Handler func(ctx context.Context, msg *schema.ActivityMsg) error +} + +func (ns *activityQueueService) Send(ctx context.Context, msg *schema.ActivityMsg) { + ns.Queue <- msg +} + +func (ns *activityQueueService) RegisterHandler( + handler func(ctx context.Context, msg *schema.ActivityMsg) error) { + ns.Handler = handler +} + +func (ns *activityQueueService) working() { + go func() { + for msg := range ns.Queue { + log.Debugf("received activity %+v", msg) + if ns.Handler == nil { + log.Warnf("no handler for activity") + continue + } + if err := ns.Handler(context.Background(), msg); err != nil { + log.Error(err) + } + } + }() +} + +// NewActivityQueueService create a new activity queue service +func NewActivityQueueService() ActivityQueueService { + ns := &activityQueueService{} + ns.Queue = make(chan *schema.ActivityMsg, 128) + ns.working() + return ns } diff --git a/internal/service/answer_common/answer.go b/internal/service/answer_common/answer.go index c23e6822..2373a6d0 100644 --- a/internal/service/answer_common/answer.go +++ b/internal/service/answer_common/answer.go @@ -3,9 +3,11 @@ package answercommon import ( "context" + "github.com/answerdev/answer/internal/base/handler" "github.com/answerdev/answer/internal/entity" "github.com/answerdev/answer/internal/schema" "github.com/answerdev/answer/pkg/htmltext" + "github.com/answerdev/answer/pkg/uid" ) type AnswerRepo interface { @@ -21,7 +23,7 @@ type AnswerRepo interface { GetCountByUserID(ctx context.Context, userID string) (int64, error) GetByUserIDQuestionID(ctx context.Context, userID string, questionID string) (*entity.Answer, bool, error) SearchList(ctx context.Context, search *entity.AnswerSearch) ([]*entity.Answer, int64, error) - AdminSearchList(ctx context.Context, search *entity.AdminAnswerSearch) ([]*entity.Answer, int64, error) + AdminSearchList(ctx context.Context, search *schema.AdminAnswerPageReq) ([]*entity.Answer, int64, error) UpdateAnswerStatus(ctx context.Context, answer *entity.Answer) (err error) GetAnswerCount(ctx context.Context) (count int64, err error) } @@ -45,11 +47,16 @@ func (as *AnswerCommon) SearchAnswered(ctx context.Context, userID, questionID s return has, nil } -func (as *AnswerCommon) AdminSearchList(ctx context.Context, search *entity.AdminAnswerSearch) ([]*entity.Answer, int64, error) { - if search.Status == 0 { - search.Status = 1 +func (as *AnswerCommon) AdminSearchList(ctx context.Context, req *schema.AdminAnswerPageReq) ( + resp []*entity.Answer, count int64, err error) { + resp, count, err = as.answerRepo.AdminSearchList(ctx, req) + if handler.GetEnableShortID(ctx) { + for _, item := range resp { + item.ID = uid.EnShortID(item.ID) + item.QuestionID = uid.EnShortID(item.QuestionID) + } } - return as.answerRepo.AdminSearchList(ctx, search) + return resp, count, err } func (as *AnswerCommon) Search(ctx context.Context, search *entity.AnswerSearch) ([]*entity.Answer, int64, error) { diff --git a/internal/service/answer_service.go b/internal/service/answer_service.go index a090bc84..b8462551 100644 --- a/internal/service/answer_service.go +++ b/internal/service/answer_service.go @@ -31,18 +31,20 @@ import ( // AnswerService user service type AnswerService struct { - answerRepo answercommon.AnswerRepo - questionRepo questioncommon.QuestionRepo - questionCommon *questioncommon.QuestionCommon - answerActivityService *activity.AnswerActivityService - userCommon *usercommon.UserCommon - collectionCommon *collectioncommon.CollectionCommon - userRepo usercommon.UserRepo - revisionService *revision_common.RevisionService - AnswerCommon *answercommon.AnswerCommon - voteRepo activity_common.VoteRepo - emailService *export.EmailService - roleService *role.UserRoleRelService + answerRepo answercommon.AnswerRepo + questionRepo questioncommon.QuestionRepo + questionCommon *questioncommon.QuestionCommon + answerActivityService *activity.AnswerActivityService + userCommon *usercommon.UserCommon + collectionCommon *collectioncommon.CollectionCommon + userRepo usercommon.UserRepo + revisionService *revision_common.RevisionService + AnswerCommon *answercommon.AnswerCommon + voteRepo activity_common.VoteRepo + emailService *export.EmailService + roleService *role.UserRoleRelService + notificationQueueService notice_queue.NotificationQueueService + activityQueueService activity_queue.ActivityQueueService } func NewAnswerService( @@ -58,20 +60,24 @@ func NewAnswerService( voteRepo activity_common.VoteRepo, emailService *export.EmailService, roleService *role.UserRoleRelService, + notificationQueueService notice_queue.NotificationQueueService, + activityQueueService activity_queue.ActivityQueueService, ) *AnswerService { return &AnswerService{ - answerRepo: answerRepo, - questionRepo: questionRepo, - userCommon: userCommon, - collectionCommon: collectionCommon, - questionCommon: questionCommon, - userRepo: userRepo, - revisionService: revisionService, - answerActivityService: answerAcceptActivityRepo, - AnswerCommon: answerCommon, - voteRepo: voteRepo, - emailService: emailService, - roleService: roleService, + answerRepo: answerRepo, + questionRepo: questionRepo, + userCommon: userCommon, + collectionCommon: collectionCommon, + questionCommon: questionCommon, + userRepo: userRepo, + revisionService: revisionService, + answerActivityService: answerAcceptActivityRepo, + AnswerCommon: answerCommon, + voteRepo: voteRepo, + emailService: emailService, + roleService: roleService, + notificationQueueService: notificationQueueService, + activityQueueService: activityQueueService, } } @@ -136,7 +142,7 @@ func (as *AnswerService) RemoveAnswer(ctx context.Context, req *schema.RemoveAns //if err != nil { // log.Errorf("delete answer activity change failed: %s", err.Error()) //} - activity_queue.AddActivity(&schema.ActivityMsg{ + as.activityQueueService.Send(ctx, &schema.ActivityMsg{ UserID: req.UserID, ObjectID: answerInfo.ID, OriginalObjectID: answerInfo.ID, @@ -205,14 +211,14 @@ func (as *AnswerService) Insert(ctx context.Context, req *schema.AnswerAddReq) ( as.notificationAnswerTheQuestion(ctx, questionInfo.UserID, questionInfo.ID, insertData.ID, req.UserID, questionInfo.Title, insertData.OriginalText) - activity_queue.AddActivity(&schema.ActivityMsg{ + as.activityQueueService.Send(ctx, &schema.ActivityMsg{ UserID: insertData.UserID, ObjectID: insertData.ID, OriginalObjectID: insertData.ID, ActivityTypeKey: constant.ActAnswerAnswered, RevisionID: revisionID, }) - activity_queue.AddActivity(&schema.ActivityMsg{ + as.activityQueueService.Send(ctx, &schema.ActivityMsg{ UserID: insertData.UserID, ObjectID: insertData.ID, OriginalObjectID: questionInfo.ID, @@ -305,7 +311,7 @@ func (as *AnswerService) Update(ctx context.Context, req *schema.AnswerUpdateReq return insertData.ID, err } if canUpdate { - activity_queue.AddActivity(&schema.ActivityMsg{ + as.activityQueueService.Send(ctx, &schema.ActivityMsg{ UserID: insertData.UserID, ObjectID: insertData.ID, OriginalObjectID: insertData.ID, @@ -384,15 +390,15 @@ func (as *AnswerService) updateAnswerRank(ctx context.Context, userID string, ) { // if this question is already been answered, should cancel old answer rank if oldAnswerInfo != nil { - err := as.answerActivityService.CancelAcceptAnswer( - ctx, questionInfo.AcceptedAnswerID, questionInfo.ID, questionInfo.UserID, oldAnswerInfo.UserID) + err := as.answerActivityService.CancelAcceptAnswer(ctx, userID, + questionInfo.AcceptedAnswerID, questionInfo.ID, questionInfo.UserID, oldAnswerInfo.UserID) if err != nil { log.Error(err) } } if newAnswerInfo.ID != "" { - err := as.answerActivityService.AcceptAnswer( - ctx, newAnswerInfo.ID, questionInfo.ID, questionInfo.UserID, newAnswerInfo.UserID, newAnswerInfo.UserID == userID) + err := as.answerActivityService.AcceptAnswer(ctx, userID, newAnswerInfo.ID, + questionInfo.ID, questionInfo.UserID, newAnswerInfo.UserID, newAnswerInfo.UserID == userID) if err != nil { log.Error(err) } @@ -472,7 +478,7 @@ func (as *AnswerService) AdminSetAnswerStatus(ctx context.Context, req *schema.A //if err != nil { // log.Errorf("admin delete question then rank rollback error %s", err.Error()) //} - activity_queue.AddActivity(&schema.ActivityMsg{ + as.activityQueueService.Send(ctx, &schema.ActivityMsg{ UserID: req.UserID, ObjectID: answerInfo.ID, OriginalObjectID: answerInfo.ID, @@ -487,7 +493,7 @@ func (as *AnswerService) AdminSetAnswerStatus(ctx context.Context, req *schema.A msg.TriggerUserID = answerInfo.UserID msg.ObjectType = constant.AnswerObjectType msg.NotificationAction = constant.NotificationYourAnswerWasDeleted - notice_queue.AddNotification(msg) + as.notificationQueueService.Send(ctx, msg) return nil } @@ -579,7 +585,7 @@ func (as *AnswerService) notificationUpdateAnswer(ctx context.Context, questionU } msg.ObjectType = constant.AnswerObjectType msg.NotificationAction = constant.NotificationUpdateAnswer - notice_queue.AddNotification(msg) + as.notificationQueueService.Send(ctx, msg) } func (as *AnswerService) notificationAnswerTheQuestion(ctx context.Context, @@ -596,7 +602,7 @@ func (as *AnswerService) notificationAnswerTheQuestion(ctx context.Context, } msg.ObjectType = constant.AnswerObjectType msg.NotificationAction = constant.NotificationAnswerTheQuestion - notice_queue.AddNotification(msg) + as.notificationQueueService.Send(ctx, msg) userInfo, exist, err := as.userRepo.GetByUserID(ctx, questionUserID) if err != nil { diff --git a/internal/service/comment/comment_service.go b/internal/service/comment/comment_service.go index a983d5d1..83970652 100644 --- a/internal/service/comment/comment_service.go +++ b/internal/service/comment/comment_service.go @@ -58,13 +58,15 @@ func (c *CommentQuery) GetOrderBy() string { // CommentService user service type CommentService struct { - commentRepo CommentRepo - commentCommonRepo comment_common.CommentCommonRepo - userCommon *usercommon.UserCommon - voteCommon activity_common.VoteRepo - objectInfoService *object_info.ObjService - emailService *export.EmailService - userRepo usercommon.UserRepo + commentRepo CommentRepo + commentCommonRepo comment_common.CommentCommonRepo + userCommon *usercommon.UserCommon + voteCommon activity_common.VoteRepo + objectInfoService *object_info.ObjService + emailService *export.EmailService + userRepo usercommon.UserRepo + notificationQueueService notice_queue.NotificationQueueService + activityQueueService activity_queue.ActivityQueueService } // NewCommentService new comment service @@ -76,15 +78,19 @@ func NewCommentService( voteCommon activity_common.VoteRepo, emailService *export.EmailService, userRepo usercommon.UserRepo, + notificationQueueService notice_queue.NotificationQueueService, + activityQueueService activity_queue.ActivityQueueService, ) *CommentService { return &CommentService{ - commentRepo: commentRepo, - commentCommonRepo: commentCommonRepo, - userCommon: userCommon, - voteCommon: voteCommon, - objectInfoService: objectInfoService, - emailService: emailService, - userRepo: userRepo, + commentRepo: commentRepo, + commentCommonRepo: commentCommonRepo, + userCommon: userCommon, + voteCommon: voteCommon, + objectInfoService: objectInfoService, + emailService: emailService, + userRepo: userRepo, + notificationQueueService: notificationQueueService, + activityQueueService: activityQueueService, } } @@ -161,7 +167,7 @@ func (cs *CommentService) AddComment(ctx context.Context, req *schema.AddComment case constant.AnswerObjectType: activityMsg.ActivityTypeKey = constant.ActAnswerCommented } - activity_queue.AddActivity(activityMsg) + cs.activityQueueService.Send(ctx, activityMsg) return resp, nil } @@ -476,7 +482,7 @@ func (cs *CommentService) notificationQuestionComment(ctx context.Context, quest } msg.ObjectType = constant.CommentObjectType msg.NotificationAction = constant.NotificationCommentQuestion - notice_queue.AddNotification(msg) + cs.notificationQueueService.Send(ctx, msg) receiverUserInfo, exist, err := cs.userRepo.GetByUserID(ctx, questionUserID) if err != nil { @@ -535,7 +541,7 @@ func (cs *CommentService) notificationAnswerComment(ctx context.Context, } msg.ObjectType = constant.CommentObjectType msg.NotificationAction = constant.NotificationCommentAnswer - notice_queue.AddNotification(msg) + cs.notificationQueueService.Send(ctx, msg) receiverUserInfo, exist, err := cs.userRepo.GetByUserID(ctx, answerUserID) if err != nil { @@ -591,7 +597,7 @@ func (cs *CommentService) notificationCommentReply(ctx context.Context, replyUse } msg.ObjectType = constant.CommentObjectType msg.NotificationAction = constant.NotificationReplyToYou - notice_queue.AddNotification(msg) + cs.notificationQueueService.Send(ctx, msg) } func (cs *CommentService) notificationMention( @@ -612,7 +618,7 @@ func (cs *CommentService) notificationMention( } msg.ObjectType = constant.CommentObjectType msg.NotificationAction = constant.NotificationMentionYou - notice_queue.AddNotification(msg) + cs.notificationQueueService.Send(ctx, msg) alreadyNotifiedUserIDs = append(alreadyNotifiedUserIDs, userInfo.ID) } } diff --git a/internal/service/comment_common/comment_service.go b/internal/service/comment_common/comment_service.go index 19c2b90a..a7250a83 100644 --- a/internal/service/comment_common/comment_service.go +++ b/internal/service/comment_common/comment_service.go @@ -35,7 +35,7 @@ func (cs *CommentCommonService) GetComment(ctx context.Context, commentID string return } if !exist { - return nil, errors.BadRequest(reason.UnknownError) + return nil, errors.BadRequest(reason.CommentNotFound) } resp = &schema.GetCommentResp{} diff --git a/internal/service/dashboard/dashboard_service.go b/internal/service/dashboard/dashboard_service.go index fa798e69..e6b26701 100644 --- a/internal/service/dashboard/dashboard_service.go +++ b/internal/service/dashboard/dashboard_service.go @@ -11,7 +11,6 @@ import ( "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/schema" "github.com/answerdev/answer/internal/service/activity_common" answercommon "github.com/answerdev/answer/internal/service/answer_common" @@ -24,11 +23,10 @@ import ( "github.com/answerdev/answer/internal/service/siteinfo_common" usercommon "github.com/answerdev/answer/internal/service/user_common" "github.com/answerdev/answer/pkg/dir" - "github.com/segmentfault/pacman/errors" "github.com/segmentfault/pacman/log" ) -type DashboardService struct { +type dashboardService struct { questionRepo questioncommon.QuestionRepo answerRepo answercommon.AnswerRepo commentRepo comment_common.CommentCommonRepo @@ -36,10 +34,9 @@ type DashboardService struct { userRepo usercommon.UserRepo reportRepo report_common.ReportRepo configService *config.ConfigService - siteInfoService *siteinfo_common.SiteInfoCommonService + siteInfoService siteinfo_common.SiteInfoCommonService serviceConfig *service_config.ServiceConfig - - data *data.Data + data *data.Data } func NewDashboardService( @@ -50,12 +47,11 @@ func NewDashboardService( userRepo usercommon.UserRepo, reportRepo report_common.ReportRepo, configService *config.ConfigService, - siteInfoService *siteinfo_common.SiteInfoCommonService, + siteInfoService siteinfo_common.SiteInfoCommonService, serviceConfig *service_config.ServiceConfig, - data *data.Data, -) *DashboardService { - return &DashboardService{ +) DashboardService { + return &dashboardService{ questionRepo: questionRepo, answerRepo: answerRepo, commentRepo: commentRepo, @@ -65,63 +61,102 @@ func NewDashboardService( configService: configService, siteInfoService: siteInfoService, serviceConfig: serviceConfig, - - data: data, + data: data, } } -func (ds *DashboardService) StatisticalByCache(ctx context.Context) (*schema.DashboardInfo, error) { - dashboardInfo := &schema.DashboardInfo{} - infoStr, err := ds.data.Cache.GetString(ctx, schema.DashBoardCachekey) +type DashboardService interface { + Statistical(ctx context.Context) (resp *schema.DashboardInfo, err error) +} + +func (ds *dashboardService) Statistical(ctx context.Context) (*schema.DashboardInfo, error) { + dashboardInfo, err := ds.getFromCache(ctx) if err != nil { - info, statisticalErr := ds.Statistical(ctx) - if statisticalErr != nil { - return nil, statisticalErr - } - if setCacheErr := ds.SetCache(ctx, info); setCacheErr != nil { - log.Errorf("set dashboard statistical failed: %s", setCacheErr) - } - return info, nil + dashboardInfo = &schema.DashboardInfo{} + dashboardInfo.QuestionCount = ds.questionCount(ctx) + dashboardInfo.AnswerCount = ds.answerCount(ctx) + dashboardInfo.CommentCount = ds.commentCount(ctx) + dashboardInfo.UserCount = ds.userCount(ctx) + dashboardInfo.ReportCount = ds.reportCount(ctx) + dashboardInfo.VoteCount = ds.voteCount(ctx) + dashboardInfo.OccupyingStorageSpace = ds.calculateStorage() + dashboardInfo.VersionInfo.RemoteVersion = ds.remoteVersion(ctx) } - if err = json.Unmarshal([]byte(infoStr), dashboardInfo); err != nil { - log.Errorf("parsing dashboard information failed: %s", err) - return nil, errors.InternalServer(reason.UnknownError) - } - startTime := time.Now().Unix() - schema.AppStartTime.Unix() - dashboardInfo.AppStartTime = fmt.Sprintf("%d", startTime) + + dashboardInfo.SMTP = ds.smtpStatus(ctx) + dashboardInfo.HTTPS = ds.httpsStatus(ctx) + dashboardInfo.TimeZone = ds.getTimezone(ctx) + dashboardInfo.UploadingFiles = true + dashboardInfo.AppStartTime = fmt.Sprintf("%d", time.Now().Unix()-schema.AppStartTime.Unix()) dashboardInfo.VersionInfo.Version = constant.Version dashboardInfo.VersionInfo.Revision = constant.Revision + + ds.setCache(ctx, dashboardInfo) return dashboardInfo, nil } -func (ds *DashboardService) SetCache(ctx context.Context, info *schema.DashboardInfo) error { - infoStr, err := json.Marshal(info) +func (ds *dashboardService) getFromCache(ctx context.Context) (*schema.DashboardInfo, error) { + infoStr, err := ds.data.Cache.GetString(ctx, schema.DashboardCacheKey) if err != nil { - return errors.InternalServer(reason.UnknownError).WithError(err).WithStack() + return nil, err } - err = ds.data.Cache.SetString(ctx, schema.DashBoardCachekey, string(infoStr), schema.DashBoardCacheTime) - if err != nil { - return errors.InternalServer(reason.UnknownError).WithError(err).WithStack() + dashboardInfo := &schema.DashboardInfo{} + if err = json.Unmarshal([]byte(infoStr), dashboardInfo); err != nil { + return nil, err } - return nil + return dashboardInfo, nil } -// Statistical -func (ds *DashboardService) Statistical(ctx context.Context) (*schema.DashboardInfo, error) { - dashboardInfo := &schema.DashboardInfo{} +func (ds *dashboardService) setCache(ctx context.Context, info *schema.DashboardInfo) { + infoStr, _ := json.Marshal(info) + err := ds.data.Cache.SetString(ctx, schema.DashboardCacheKey, string(infoStr), schema.DashboardCacheTime) + if err != nil { + log.Errorf("set dashboard statistical failed: %s", err) + } +} + +func (ds *dashboardService) questionCount(ctx context.Context) int64 { questionCount, err := ds.questionRepo.GetQuestionCount(ctx) if err != nil { - return dashboardInfo, err + log.Errorf("get question count failed: %s", err) } + return questionCount +} + +func (ds *dashboardService) answerCount(ctx context.Context) int64 { answerCount, err := ds.answerRepo.GetAnswerCount(ctx) if err != nil { - return dashboardInfo, err + log.Errorf("get answer count failed: %s", err) } + return answerCount +} + +func (ds *dashboardService) commentCount(ctx context.Context) int64 { commentCount, err := ds.commentRepo.GetCommentCount(ctx) if err != nil { - return dashboardInfo, err + log.Errorf("get comment count failed: %s", err) } + return commentCount +} +func (ds *dashboardService) userCount(ctx context.Context) int64 { + userCount, err := ds.userRepo.GetUserCount(ctx) + if err != nil { + log.Errorf("get user count failed: %s", err) + } + return userCount +} + +func (ds *dashboardService) reportCount(ctx context.Context) int64 { + reportCount, err := ds.reportRepo.GetReportCount(ctx) + if err != nil { + log.Errorf("get report count failed: %s", err) + } + return reportCount +} + +// count vote +func (ds *dashboardService) voteCount(ctx context.Context) int64 { typeKeys := []string{ "question.vote_up", "question.vote_down", @@ -129,7 +164,6 @@ func (ds *DashboardService) Statistical(ctx context.Context) (*schema.DashboardI "answer.vote_down", } var activityTypes []int - for _, typeKey := range typeKeys { cfg, err := ds.configService.GetConfigByKey(ctx, typeKey) if err != nil { @@ -137,69 +171,14 @@ func (ds *DashboardService) Statistical(ctx context.Context) (*schema.DashboardI } activityTypes = append(activityTypes, cfg.ID) } - voteCount, err := ds.voteRepo.GetVoteCount(ctx, activityTypes) if err != nil { - return dashboardInfo, err + log.Errorf("get vote count failed: %s", err) } - userCount, err := ds.userRepo.GetUserCount(ctx) - if err != nil { - return dashboardInfo, err - } - - reportCount, err := ds.reportRepo.GetReportCount(ctx) - if err != nil { - return dashboardInfo, err - } - - siteInfoInterface, err := ds.siteInfoService.GetSiteInterface(ctx) - if err != nil { - return dashboardInfo, err - } - - dashboardInfo.QuestionCount = questionCount - dashboardInfo.AnswerCount = answerCount - dashboardInfo.CommentCount = commentCount - dashboardInfo.VoteCount = voteCount - dashboardInfo.UserCount = userCount - dashboardInfo.ReportCount = reportCount - - dashboardInfo.UploadingFiles = true - emailconfig, err := ds.GetEmailConfig(ctx) - if err != nil { - return dashboardInfo, err - } - if emailconfig.SMTPHost != "" { - dashboardInfo.SMTP = true - } - siteGeneral, err := ds.siteInfoService.GetSiteGeneral(ctx) - if err != nil { - return dashboardInfo, err - } - siteUrl, err := url.Parse(siteGeneral.SiteUrl) - if err != nil { - return dashboardInfo, err - } - if siteUrl.Scheme == "https" { - dashboardInfo.HTTPS = true - } - - dirSize, err := dir.DirSize(ds.serviceConfig.UploadPath) - if err != nil { - return dashboardInfo, err - } - size := dir.FormatFileSize(dirSize) - dashboardInfo.OccupyingStorageSpace = size - startTime := time.Now().Unix() - schema.AppStartTime.Unix() - dashboardInfo.AppStartTime = fmt.Sprintf("%d", startTime) - dashboardInfo.TimeZone = siteInfoInterface.TimeZone - dashboardInfo.VersionInfo.Version = constant.Version - dashboardInfo.VersionInfo.Revision = constant.Revision - dashboardInfo.VersionInfo.RemoteVersion = ds.RemoteVersion(ctx) - return dashboardInfo, nil + return voteCount } -func (ds *DashboardService) RemoteVersion(ctx context.Context) string { +func (ds *dashboardService) remoteVersion(ctx context.Context) string { url := "https://answer.dev/getlatest" req, _ := http.NewRequest("GET", url, nil) req.Header.Set("User-Agent", "Answer/"+constant.Version) @@ -224,15 +203,48 @@ func (ds *DashboardService) RemoteVersion(ctx context.Context) string { return remoteVersion.Release.Version } -func (ds *DashboardService) GetEmailConfig(ctx context.Context) (ec *export.EmailConfig, err error) { +func (ds *dashboardService) smtpStatus(ctx context.Context) (enabled bool) { emailConf, err := ds.configService.GetStringValue(ctx, "email.config") if err != nil { - return nil, err + log.Errorf("get email config failed: %s", err) + return false } - ec = &export.EmailConfig{} + ec := &export.EmailConfig{} err = json.Unmarshal([]byte(emailConf), ec) if err != nil { - return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + log.Errorf("parsing email config failed: %s", err) + return false } - return ec, nil + return ec.SMTPHost != "" +} + +func (ds *dashboardService) httpsStatus(ctx context.Context) (enabled bool) { + siteGeneral, err := ds.siteInfoService.GetSiteGeneral(ctx) + if err != nil { + log.Errorf("get site general failed: %s", err) + return false + } + siteUrl, err := url.Parse(siteGeneral.SiteUrl) + if err != nil { + log.Errorf("parse site url failed: %s", err) + return false + } + return siteUrl.Scheme == "https" +} + +func (ds *dashboardService) getTimezone(ctx context.Context) string { + siteInfoInterface, err := ds.siteInfoService.GetSiteInterface(ctx) + if err != nil { + return "" + } + return siteInfoInterface.TimeZone +} + +func (ds *dashboardService) calculateStorage() string { + dirSize, err := dir.DirSize(ds.serviceConfig.UploadPath) + if err != nil { + log.Errorf("get upload dir size failed: %s", err) + return "" + } + return dir.FormatFileSize(dirSize) } diff --git a/internal/service/export/email_service.go b/internal/service/export/email_service.go index 2812aa66..1e8297c7 100644 --- a/internal/service/export/email_service.go +++ b/internal/service/export/email_service.go @@ -79,6 +79,14 @@ type TestTemplateData struct { SiteName string } +// SaveCode save code +func (es *EmailService) SaveCode(ctx context.Context, code, codeContent string) { + err := es.emailRepo.SetCode(ctx, code, codeContent, 10*time.Minute) + if err != nil { + log.Error(err) + } +} + // SendAndSaveCode send email and save code func (es *EmailService) SendAndSaveCode(ctx context.Context, toEmailAddr, subject, body, code, codeContent string) { es.Send(ctx, toEmailAddr, subject, body) @@ -106,6 +114,10 @@ func (es *EmailService) Send(ctx context.Context, toEmailAddr, subject, body str log.Errorf("get email config failed: %s", err) return } + if len(ec.SMTPHost) == 0 { + log.Warnf("smtp host is empty, skip send email") + return + } m := gomail.NewMessage() fromName := mime.QEncoding.Encode("utf-8", ec.FromName) diff --git a/internal/service/notice_queue/notice_queue.go b/internal/service/notice_queue/notice_queue.go index 2281a7ee..78f6fa50 100644 --- a/internal/service/notice_queue/notice_queue.go +++ b/internal/service/notice_queue/notice_queue.go @@ -1,13 +1,50 @@ package notice_queue import ( + "context" + "github.com/answerdev/answer/internal/schema" + "github.com/segmentfault/pacman/log" ) -var ( - NotificationQueue = make(chan *schema.NotificationMsg, 128) -) - -func AddNotification(msg *schema.NotificationMsg) { - NotificationQueue <- msg +type NotificationQueueService interface { + Send(ctx context.Context, msg *schema.NotificationMsg) + RegisterHandler(handler func(ctx context.Context, msg *schema.NotificationMsg) error) +} + +type notificationQueueService struct { + Queue chan *schema.NotificationMsg + Handler func(ctx context.Context, msg *schema.NotificationMsg) error +} + +func (ns *notificationQueueService) Send(ctx context.Context, msg *schema.NotificationMsg) { + ns.Queue <- msg +} + +func (ns *notificationQueueService) RegisterHandler( + handler func(ctx context.Context, msg *schema.NotificationMsg) error) { + ns.Handler = handler +} + +func (ns *notificationQueueService) working() { + go func() { + for msg := range ns.Queue { + log.Debugf("received notification %+v", msg) + if ns.Handler == nil { + log.Warnf("no handler for notification") + continue + } + if err := ns.Handler(context.Background(), msg); err != nil { + log.Error(err) + } + } + }() +} + +// NewNotificationQueueService create a new notification queue service +func NewNotificationQueueService() NotificationQueueService { + ns := ¬ificationQueueService{} + ns.Queue = make(chan *schema.NotificationMsg, 128) + ns.working() + return ns } diff --git a/internal/service/notification/notification_service.go b/internal/service/notification/notification_service.go index 6e7d09d6..25d8444e 100644 --- a/internal/service/notification/notification_service.go +++ b/internal/service/notification/notification_service.go @@ -146,6 +146,7 @@ func (ns *NotificationService) GetNotificationPage(ctx context.Context, searchCo func (ns *NotificationService) formatNotificationPage(ctx context.Context, notifications []*entity.Notification) ( resp []*schema.NotificationContent, err error) { lang := handler.GetLangByCtx(ctx) + enableShortID := handler.GetEnableShortID(ctx) for _, notificationInfo := range notifications { item := &schema.NotificationContent{} if err := json.Unmarshal([]byte(notificationInfo.Content), item); err != nil { @@ -163,17 +164,19 @@ func (ns *NotificationService) formatNotificationPage(ctx context.Context, notif item.UpdateTime = notificationInfo.UpdatedAt.Unix() item.IsRead = notificationInfo.IsRead == schema.NotificationRead - if answerID, ok := item.ObjectInfo.ObjectMap["answer"]; ok { - if item.ObjectInfo.ObjectID == answerID { - item.ObjectInfo.ObjectID = uid.EnShortID(item.ObjectInfo.ObjectMap["answer"]) + if enableShortID { + if answerID, ok := item.ObjectInfo.ObjectMap["answer"]; ok { + if item.ObjectInfo.ObjectID == answerID { + item.ObjectInfo.ObjectID = uid.EnShortID(item.ObjectInfo.ObjectMap["answer"]) + } + item.ObjectInfo.ObjectMap["answer"] = uid.EnShortID(item.ObjectInfo.ObjectMap["answer"]) } - item.ObjectInfo.ObjectMap["answer"] = uid.EnShortID(item.ObjectInfo.ObjectMap["answer"]) - } - if questionID, ok := item.ObjectInfo.ObjectMap["question"]; ok { - if item.ObjectInfo.ObjectID == questionID { - item.ObjectInfo.ObjectID = uid.EnShortID(item.ObjectInfo.ObjectMap["question"]) + if questionID, ok := item.ObjectInfo.ObjectMap["question"]; ok { + if item.ObjectInfo.ObjectID == questionID { + item.ObjectInfo.ObjectID = uid.EnShortID(item.ObjectInfo.ObjectMap["question"]) + } + item.ObjectInfo.ObjectMap["question"] = uid.EnShortID(item.ObjectInfo.ObjectMap["question"]) } - item.ObjectInfo.ObjectMap["question"] = uid.EnShortID(item.ObjectInfo.ObjectMap["question"]) } resp = append(resp, item) diff --git a/internal/service/notification_common/notification.go b/internal/service/notification_common/notification.go index 2bf0cf8c..ed7001a9 100644 --- a/internal/service/notification_common/notification.go +++ b/internal/service/notification_common/notification.go @@ -33,12 +33,13 @@ type NotificationRepo interface { } type NotificationCommon struct { - data *data.Data - notificationRepo NotificationRepo - activityRepo activity_common.ActivityRepo - followRepo activity_common.FollowRepo - userCommon *usercommon.UserCommon - objectInfoService *object_info.ObjService + data *data.Data + notificationRepo NotificationRepo + activityRepo activity_common.ActivityRepo + followRepo activity_common.FollowRepo + userCommon *usercommon.UserCommon + objectInfoService *object_info.ObjService + notificationQueueService notice_queue.NotificationQueueService } func NewNotificationCommon( @@ -48,31 +49,21 @@ func NewNotificationCommon( activityRepo activity_common.ActivityRepo, followRepo activity_common.FollowRepo, objectInfoService *object_info.ObjService, + notificationQueueService notice_queue.NotificationQueueService, ) *NotificationCommon { notification := &NotificationCommon{ - data: data, - notificationRepo: notificationRepo, - activityRepo: activityRepo, - followRepo: followRepo, - userCommon: userCommon, - objectInfoService: objectInfoService, + data: data, + notificationRepo: notificationRepo, + activityRepo: activityRepo, + followRepo: followRepo, + userCommon: userCommon, + objectInfoService: objectInfoService, + notificationQueueService: notificationQueueService, } - notification.HandleNotification() + notificationQueueService.RegisterHandler(notification.AddNotification) return notification } -func (ns *NotificationCommon) HandleNotification() { - go func() { - for msg := range notice_queue.NotificationQueue { - log.Debugf("received notification %+v", msg) - err := ns.AddNotification(context.TODO(), msg) - if err != nil { - log.Error(err) - } - } - }() -} - // AddNotification // need set // LoginUserID @@ -172,7 +163,7 @@ func (ns *NotificationCommon) AddNotification(ctx context.Context, msg *schema.N log.Error("addRedDot Error", err.Error()) } - go ns.SendNotificationToAllFollower(context.Background(), msg, questionID) + go ns.SendNotificationToAllFollower(ctx, msg, questionID) return nil } @@ -213,6 +204,6 @@ func (ns *NotificationCommon) SendNotificationToAllFollower(ctx context.Context, t.ReceiverUserID = userID t.TriggerUserID = msg.TriggerUserID t.NoNeedPushAllFollow = true - notice_queue.AddNotification(t) + ns.notificationQueueService.Send(ctx, t) } } diff --git a/internal/service/object_info/object_info.go b/internal/service/object_info/object_info.go index feda14f9..09e59882 100644 --- a/internal/service/object_info/object_info.go +++ b/internal/service/object_info/object_info.go @@ -4,6 +4,7 @@ import ( "context" "github.com/answerdev/answer/internal/base/constant" + "github.com/answerdev/answer/internal/base/handler" "github.com/answerdev/answer/internal/base/reason" "github.com/answerdev/answer/internal/schema" answercommon "github.com/answerdev/answer/internal/service/answer_common" @@ -51,7 +52,9 @@ func (os *ObjService) GetUnreviewedRevisionInfo(ctx context.Context, objectID st if err != nil { return nil, err } - questionInfo.ID = uid.EnShortID(questionInfo.ID) + if handler.GetEnableShortID(ctx) { + questionInfo.ID = uid.EnShortID(questionInfo.ID) + } if !exist { break } @@ -87,7 +90,9 @@ func (os *ObjService) GetUnreviewedRevisionInfo(ctx context.Context, objectID st if !exist { break } - questionInfo.ID = uid.EnShortID(questionInfo.ID) + if handler.GetEnableShortID(ctx) { + questionInfo.ID = uid.EnShortID(questionInfo.ID) + } objInfo = &schema.UnreviewedRevisionInfoInfo{ ObjectID: answerInfo.ID, Title: questionInfo.Title, diff --git a/internal/service/permission/question_permission.go b/internal/service/permission/question_permission.go index 8f9c631d..0bcde13d 100644 --- a/internal/service/permission/question_permission.go +++ b/internal/service/permission/question_permission.go @@ -83,12 +83,11 @@ func GetQuestionPermission(ctx context.Context, userID string, creatorUserID str } // GetQuestionExtendsPermission get question extends permission -func GetQuestionExtendsPermission(ctx context.Context, userID string, creatorUserID string, - canInviteOtherToAnswer bool) ( +func GetQuestionExtendsPermission(ctx context.Context, canInviteOtherToAnswer bool) ( actions []*schema.PermissionMemberAction) { lang := handler.GetLangByCtx(ctx) actions = make([]*schema.PermissionMemberAction, 0) - if canInviteOtherToAnswer || userID == creatorUserID { + if canInviteOtherToAnswer { actions = append(actions, &schema.PermissionMemberAction{ Action: "invite_other_to_answer", Name: translator.Tr(lang, inviteSomeoneToAnswerActionName), diff --git a/internal/service/provider.go b/internal/service/provider.go index 69c30047..53990b91 100644 --- a/internal/service/provider.go +++ b/internal/service/provider.go @@ -4,6 +4,7 @@ import ( "github.com/answerdev/answer/internal/service/action" "github.com/answerdev/answer/internal/service/activity" "github.com/answerdev/answer/internal/service/activity_common" + "github.com/answerdev/answer/internal/service/activity_queue" answercommon "github.com/answerdev/answer/internal/service/answer_common" "github.com/answerdev/answer/internal/service/auth" collectioncommon "github.com/answerdev/answer/internal/service/collection_common" @@ -14,6 +15,7 @@ import ( "github.com/answerdev/answer/internal/service/export" "github.com/answerdev/answer/internal/service/follow" "github.com/answerdev/answer/internal/service/meta" + "github.com/answerdev/answer/internal/service/notice_queue" "github.com/answerdev/answer/internal/service/notification" notficationcommon "github.com/answerdev/answer/internal/service/notification_common" "github.com/answerdev/answer/internal/service/object_info" @@ -86,4 +88,6 @@ var ProviderSetService = wire.NewSet( user_external_login.NewUserCenterLoginService, plugin_common.NewPluginCommonService, config.NewConfigService, + notice_queue.NewNotificationQueueService, + activity_queue.NewActivityQueueService, ) diff --git a/internal/service/question_common/question.go b/internal/service/question_common/question.go index b0ccc4cc..84a6c880 100644 --- a/internal/service/question_common/question.go +++ b/internal/service/question_common/question.go @@ -3,7 +3,6 @@ package questioncommon import ( "context" "encoding/json" - "fmt" "math" "time" @@ -48,26 +47,26 @@ type QuestionRepo interface { UpdateAccepted(ctx context.Context, question *entity.Question) (err error) UpdateLastAnswer(ctx context.Context, question *entity.Question) (err error) FindByID(ctx context.Context, id []string) (questionList []*entity.Question, err error) - AdminSearchList(ctx context.Context, search *schema.AdminQuestionSearch) ([]*entity.Question, int64, error) + AdminQuestionPage(ctx context.Context, search *schema.AdminQuestionPageReq) ([]*entity.Question, int64, error) GetQuestionCount(ctx context.Context) (count int64, err error) GetUserQuestionCount(ctx context.Context, userID string) (count int64, err error) - GetQuestionCountByIDs(ctx context.Context, ids []string) (count int64, err error) - GetQuestionIDsPage(ctx context.Context, page, pageSize int) (questionIDList []*schema.SiteMapQuestionInfo, err error) + SitemapQuestions(ctx context.Context, page, pageSize int) (questionIDList []*schema.SiteMapQuestionInfo, err error) } // QuestionCommon user service type QuestionCommon struct { - questionRepo QuestionRepo - answerRepo answercommon.AnswerRepo - voteRepo activity_common.VoteRepo - followCommon activity_common.FollowRepo - tagCommon *tagcommon.TagCommonService - userCommon *usercommon.UserCommon - collectionCommon *collectioncommon.CollectionCommon - AnswerCommon *answercommon.AnswerCommon - metaService *meta.MetaService - configService *config.ConfigService - data *data.Data + questionRepo QuestionRepo + answerRepo answercommon.AnswerRepo + voteRepo activity_common.VoteRepo + followCommon activity_common.FollowRepo + tagCommon *tagcommon.TagCommonService + userCommon *usercommon.UserCommon + collectionCommon *collectioncommon.CollectionCommon + AnswerCommon *answercommon.AnswerCommon + metaService *meta.MetaService + configService *config.ConfigService + activityQueueService activity_queue.ActivityQueueService + data *data.Data } func NewQuestionCommon(questionRepo QuestionRepo, @@ -80,21 +79,22 @@ func NewQuestionCommon(questionRepo QuestionRepo, answerCommon *answercommon.AnswerCommon, metaService *meta.MetaService, configService *config.ConfigService, + activityQueueService activity_queue.ActivityQueueService, data *data.Data, - ) *QuestionCommon { return &QuestionCommon{ - questionRepo: questionRepo, - answerRepo: answerRepo, - voteRepo: voteRepo, - followCommon: followCommon, - tagCommon: tagCommon, - userCommon: userCommon, - collectionCommon: collectionCommon, - AnswerCommon: answerCommon, - metaService: metaService, - configService: configService, - data: data, + questionRepo: questionRepo, + answerRepo: answerRepo, + voteRepo: voteRepo, + followCommon: followCommon, + tagCommon: tagCommon, + userCommon: userCommon, + collectionCommon: collectionCommon, + AnswerCommon: answerCommon, + metaService: metaService, + configService: configService, + activityQueueService: activityQueueService, + data: data, } } @@ -513,7 +513,7 @@ func (qs *QuestionCommon) CloseQuestion(ctx context.Context, req *schema.CloseQu return err } - activity_queue.AddActivity(&schema.ActivityMsg{ + qs.activityQueueService.Send(ctx, &schema.ActivityMsg{ UserID: questionInfo.UserID, ObjectID: questionInfo.ID, OriginalObjectID: questionInfo.ID, @@ -551,40 +551,26 @@ func (as *QuestionCommon) RemoveAnswer(ctx context.Context, id string) (err erro } func (qs *QuestionCommon) SitemapCron(ctx context.Context) { - data := &schema.SiteMapList{} questionNum, err := qs.questionRepo.GetQuestionCount(ctx) if err != nil { - log.Error("GetQuestionCount error", err) + log.Error(err) return } - if questionNum <= schema.SitemapMaxSize { - questionIDList, err := qs.questionRepo.GetQuestionIDsPage(ctx, 0, int(questionNum)) + if questionNum <= constant.SitemapMaxSize { + _, err = qs.questionRepo.SitemapQuestions(ctx, 1, int(questionNum)) if err != nil { - log.Error("GetQuestionIDsPage error", err) + log.Errorf("get site map question error: %v", err) + } + return + } + + totalPages := int(math.Ceil(float64(questionNum) / float64(constant.SitemapMaxSize))) + for i := 1; i <= totalPages; i++ { + _, err = qs.questionRepo.SitemapQuestions(ctx, i, constant.SitemapMaxSize) + if err != nil { + log.Errorf("get site map question error: %v", 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) } } @@ -594,7 +580,7 @@ func (qs *QuestionCommon) SetCache(ctx context.Context, cachekey string, info in return errors.InternalServer(reason.UnknownError).WithError(err).WithStack() } - err = qs.data.Cache.SetString(ctx, cachekey, string(infoStr), schema.DashBoardCacheTime) + err = qs.data.Cache.SetString(ctx, cachekey, string(infoStr), schema.DashboardCacheTime) if err != nil { return errors.InternalServer(reason.UnknownError).WithError(err).WithStack() } diff --git a/internal/service/question_service.go b/internal/service/question_service.go index 378b27df..6fdd3852 100644 --- a/internal/service/question_service.go +++ b/internal/service/question_service.go @@ -3,11 +3,11 @@ package service import ( "encoding/json" "fmt" + "github.com/answerdev/answer/internal/service/siteinfo_common" "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/pager" "github.com/answerdev/answer/internal/base/reason" @@ -41,17 +41,19 @@ import ( // QuestionService user service type QuestionService struct { - questionRepo questioncommon.QuestionRepo - tagCommon *tagcommon.TagCommonService - questioncommon *questioncommon.QuestionCommon - userCommon *usercommon.UserCommon - userRepo usercommon.UserRepo - revisionService *revision_common.RevisionService - metaService *meta.MetaService - collectionCommon *collectioncommon.CollectionCommon - answerActivityService *activity.AnswerActivityService - data *data.Data - emailService *export.EmailService + questionRepo questioncommon.QuestionRepo + tagCommon *tagcommon.TagCommonService + questioncommon *questioncommon.QuestionCommon + userCommon *usercommon.UserCommon + userRepo usercommon.UserRepo + revisionService *revision_common.RevisionService + metaService *meta.MetaService + collectionCommon *collectioncommon.CollectionCommon + answerActivityService *activity.AnswerActivityService + emailService *export.EmailService + notificationQueueService notice_queue.NotificationQueueService + activityQueueService activity_queue.ActivityQueueService + siteInfoService siteinfo_common.SiteInfoCommonService } func NewQuestionService( @@ -64,21 +66,25 @@ func NewQuestionService( metaService *meta.MetaService, collectionCommon *collectioncommon.CollectionCommon, answerActivityService *activity.AnswerActivityService, - data *data.Data, emailService *export.EmailService, + notificationQueueService notice_queue.NotificationQueueService, + activityQueueService activity_queue.ActivityQueueService, + siteInfoService siteinfo_common.SiteInfoCommonService, ) *QuestionService { return &QuestionService{ - questionRepo: questionRepo, - tagCommon: tagCommon, - questioncommon: questioncommon, - userCommon: userCommon, - userRepo: userRepo, - revisionService: revisionService, - metaService: metaService, - collectionCommon: collectionCommon, - answerActivityService: answerActivityService, - data: data, - emailService: emailService, + questionRepo: questionRepo, + tagCommon: tagCommon, + questioncommon: questioncommon, + userCommon: userCommon, + userRepo: userRepo, + revisionService: revisionService, + metaService: metaService, + collectionCommon: collectionCommon, + answerActivityService: answerActivityService, + emailService: emailService, + notificationQueueService: notificationQueueService, + activityQueueService: activityQueueService, + siteInfoService: siteInfoService, } } @@ -106,7 +112,7 @@ func (qs *QuestionService) CloseQuestion(ctx context.Context, req *schema.CloseQ return err } - activity_queue.AddActivity(&schema.ActivityMsg{ + qs.activityQueueService.Send(ctx, &schema.ActivityMsg{ UserID: req.UserID, ObjectID: questionInfo.ID, OriginalObjectID: questionInfo.ID, @@ -130,7 +136,7 @@ func (qs *QuestionService) ReopenQuestion(ctx context.Context, req *schema.Reope if err != nil { return err } - activity_queue.AddActivity(&schema.ActivityMsg{ + qs.activityQueueService.Send(ctx, &schema.ActivityMsg{ UserID: req.UserID, ObjectID: questionInfo.ID, OriginalObjectID: questionInfo.ID, @@ -312,7 +318,7 @@ func (qs *QuestionService) AddQuestion(ctx context.Context, req *schema.Question } } - activity_queue.AddActivity(&schema.ActivityMsg{ + qs.activityQueueService.Send(ctx, &schema.ActivityMsg{ UserID: question.UserID, ObjectID: question.ID, OriginalObjectID: question.ID, @@ -381,7 +387,7 @@ func (qs *QuestionService) OperationQuestion(ctx context.Context, req *schema.Op actMap[schema.QuestionOperationShow] = constant.ActQuestionShow _, ok := actMap[req.Operation] if ok { - activity_queue.AddActivity(&schema.ActivityMsg{ + qs.activityQueueService.Send(ctx, &schema.ActivityMsg{ UserID: req.UserID, ObjectID: questionInfo.ID, OriginalObjectID: questionInfo.ID, @@ -473,7 +479,7 @@ func (qs *QuestionService) RemoveQuestion(ctx context.Context, req *schema.Remov // if err != nil { // log.Errorf("user DeleteQuestion rank rollback error %s", err.Error()) // } - activity_queue.AddActivity(&schema.ActivityMsg{ + qs.activityQueueService.Send(ctx, &schema.ActivityMsg{ UserID: req.UserID, ObjectID: questionInfo.ID, OriginalObjectID: questionInfo.ID, @@ -632,7 +638,7 @@ func (qs *QuestionService) notificationInviteUser( } msg.ObjectType = constant.QuestionObjectType msg.NotificationAction = constant.NotificationInvitedYouToAnswer - notice_queue.AddNotification(msg) + qs.notificationQueueService.Send(ctx, msg) userInfo, ok := invitee[userID] if !ok { @@ -822,7 +828,7 @@ func (qs *QuestionService) UpdateQuestion(ctx context.Context, req *schema.Quest return } if canUpdate { - activity_queue.AddActivity(&schema.ActivityMsg{ + qs.activityQueueService.Send(ctx, &schema.ActivityMsg{ UserID: req.UserID, ObjectID: question.ID, ActivityTypeKey: constant.ActQuestionEdited, @@ -877,7 +883,7 @@ func (qs *QuestionService) GetQuestion(ctx context.Context, questionID, userID s question.Description = htmltext.FetchExcerpt(question.HTML, "...", 240) question.MemberActions = permission.GetQuestionPermission(ctx, userID, question.UserID, per.CanEdit, per.CanDelete, per.CanClose, per.CanReopen, per.CanPin, per.CanHide, per.CanUnPin, per.CanShow) - question.ExtendsActions = permission.GetQuestionExtendsPermission(ctx, userID, question.UserID, per.CanInviteOtherToAnswer) + question.ExtendsActions = permission.GetQuestionExtendsPermission(ctx, per.CanInviteOtherToAnswer) return question, nil } @@ -1017,16 +1023,19 @@ func (qs *QuestionService) PersonalCollectionPage(ctx context.Context, req *sche return nil, err } for _, id := range questionIDs { - _, ok := questionMaps[uid.EnShortID(id)] + if handler.GetEnableShortID(ctx) { + id = uid.EnShortID(id) + } + _, ok := questionMaps[id] if ok { - questionMaps[uid.EnShortID(id)].LastAnsweredUserInfo = nil - questionMaps[uid.EnShortID(id)].UpdateUserInfo = nil - questionMaps[uid.EnShortID(id)].Content = "" - questionMaps[uid.EnShortID(id)].HTML = "" - if questionMaps[uid.EnShortID(id)].Status == entity.QuestionStatusDeleted { - questionMaps[uid.EnShortID(id)].Title = "Deleted question" + questionMaps[id].LastAnsweredUserInfo = nil + questionMaps[id].UpdateUserInfo = nil + questionMaps[id].Content = "" + questionMaps[id].HTML = "" + if questionMaps[id].Status == entity.QuestionStatusDeleted { + questionMaps[id].Title = "Deleted question" } - list = append(list, questionMaps[uid.EnShortID(id)]) + list = append(list, questionMaps[id]) } } @@ -1213,7 +1222,7 @@ func (qs *QuestionService) AdminSetQuestionStatus(ctx context.Context, questionI //if err != nil { // log.Errorf("admin delete question then rank rollback error %s", err.Error()) //} - activity_queue.AddActivity(&schema.ActivityMsg{ + qs.activityQueueService.Send(ctx, &schema.ActivityMsg{ UserID: questionInfo.UserID, ObjectID: questionInfo.ID, OriginalObjectID: questionInfo.ID, @@ -1221,7 +1230,7 @@ func (qs *QuestionService) AdminSetQuestionStatus(ctx context.Context, questionI }) } if setStatus == entity.QuestionStatusAvailable && questionInfo.Status == entity.QuestionStatusClosed { - activity_queue.AddActivity(&schema.ActivityMsg{ + qs.activityQueueService.Send(ctx, &schema.ActivityMsg{ UserID: questionInfo.UserID, ObjectID: questionInfo.ID, OriginalObjectID: questionInfo.ID, @@ -1229,7 +1238,7 @@ func (qs *QuestionService) AdminSetQuestionStatus(ctx context.Context, questionI }) } if setStatus == entity.QuestionStatusClosed && questionInfo.Status != entity.QuestionStatusClosed { - activity_queue.AddActivity(&schema.ActivityMsg{ + qs.activityQueueService.Send(ctx, &schema.ActivityMsg{ UserID: questionInfo.UserID, ObjectID: questionInfo.ID, OriginalObjectID: questionInfo.ID, @@ -1243,93 +1252,77 @@ func (qs *QuestionService) AdminSetQuestionStatus(ctx context.Context, questionI msg.TriggerUserID = questionInfo.UserID msg.ObjectType = constant.QuestionObjectType msg.NotificationAction = constant.NotificationYourQuestionWasDeleted - notice_queue.AddNotification(msg) + qs.notificationQueueService.Send(ctx, msg) return nil } -func (qs *QuestionService) AdminSearchList(ctx context.Context, search *schema.AdminQuestionSearch, loginUserID string) ([]*schema.AdminQuestionInfo, int64, error) { +func (qs *QuestionService) AdminQuestionPage( + ctx context.Context, req *schema.AdminQuestionPageReq) ( + resp *pager.PageModel, err error) { + list := make([]*schema.AdminQuestionInfo, 0) - - status, ok := entity.AdminQuestionSearchStatus[search.StatusStr] - if ok { - search.Status = status - } - - if search.Status == 0 { - search.Status = 1 - } - dblist, count, err := qs.questionRepo.AdminSearchList(ctx, search) + questionList, count, err := qs.questionRepo.AdminQuestionPage(ctx, req) if err != nil { - return list, count, err + return nil, err } + userIds := make([]string, 0) - for _, dbitem := range dblist { + for _, info := range questionList { item := &schema.AdminQuestionInfo{} - _ = copier.Copy(item, dbitem) - item.CreateTime = dbitem.CreatedAt.Unix() - item.UpdateTime = dbitem.PostUpdateTime.Unix() - item.EditTime = dbitem.UpdatedAt.Unix() + _ = copier.Copy(item, info) + item.CreateTime = info.CreatedAt.Unix() + item.UpdateTime = info.PostUpdateTime.Unix() + item.EditTime = info.UpdatedAt.Unix() list = append(list, item) - userIds = append(userIds, dbitem.UserID) + userIds = append(userIds, info.UserID) } userInfoMap, err := qs.userCommon.BatchUserBasicInfoByID(ctx, userIds) if err != nil { - return list, count, err + return nil, err } for _, item := range list { - _, ok = userInfoMap[item.UserID] - if ok { - item.UserInfo = userInfoMap[item.UserID] + if u, ok := userInfoMap[item.UserID]; ok { + item.UserInfo = u } } - - return list, count, nil + return pager.NewPageModel(count, list), nil } -// AdminSearchList -func (qs *QuestionService) AdminSearchAnswerList(ctx context.Context, search *entity.AdminAnswerSearch, loginUserID string) ([]*schema.AdminAnswerInfo, int64, error) { - answerlist := make([]*schema.AdminAnswerInfo, 0) - - status, ok := entity.AdminAnswerSearchStatus[search.StatusStr] - if ok { - search.Status = status - } - - if search.Status == 0 { - search.Status = 1 - } - dblist, count, err := qs.questioncommon.AnswerCommon.AdminSearchList(ctx, search) +// AdminAnswerPage search answer list +func (qs *QuestionService) AdminAnswerPage(ctx context.Context, req *schema.AdminAnswerPageReq) ( + resp *pager.PageModel, err error) { + answerList, count, err := qs.questioncommon.AnswerCommon.AdminSearchList(ctx, req) if err != nil { - return answerlist, count, err + return nil, err } + questionIDs := make([]string, 0) userIds := make([]string, 0) - for _, item := range dblist { - answerinfo := qs.questioncommon.AnswerCommon.AdminShowFormat(ctx, item) - answerlist = append(answerlist, answerinfo) + answerResp := make([]*schema.AdminAnswerInfo, 0) + for _, item := range answerList { + answerInfo := qs.questioncommon.AnswerCommon.AdminShowFormat(ctx, item) + answerResp = append(answerResp, answerInfo) questionIDs = append(questionIDs, item.QuestionID) userIds = append(userIds, item.UserID) } userInfoMap, err := qs.userCommon.BatchUserBasicInfoByID(ctx, userIds) if err != nil { - return answerlist, count, err + return nil, err + } + questionMaps, err := qs.questioncommon.FindInfoByID(ctx, questionIDs, req.LoginUserID) + if err != nil { + return nil, err } - questionMaps, err := qs.questioncommon.FindInfoByID(ctx, questionIDs, loginUserID) - if err != nil { - return answerlist, count, err - } - for _, item := range answerlist { - _, ok := questionMaps[item.QuestionID] - if ok { - item.QuestionInfo.Title = questionMaps[item.QuestionID].Title + for _, item := range answerResp { + if q, ok := questionMaps[item.QuestionID]; ok { + item.QuestionInfo.Title = q.Title } - _, ok = userInfoMap[item.UserID] - if ok { - item.UserInfo = userInfoMap[item.UserID] + if u, ok := userInfoMap[item.UserID]; ok { + item.UserInfo = u } } - return answerlist, count, nil + return pager.NewPageModel(count, answerResp), nil } func (qs *QuestionService) changeQuestionToRevision(ctx context.Context, questionInfo *entity.Question, tags []*entity.Tag) ( @@ -1346,5 +1339,11 @@ func (qs *QuestionService) changeQuestionToRevision(ctx context.Context, questio } func (qs *QuestionService) SitemapCron(ctx context.Context) { + siteSeo, err := qs.siteInfoService.GetSiteSeo(ctx) + if err != nil { + log.Error(err) + return + } + ctx = context.WithValue(ctx, constant.ShortIDFlag, siteSeo.IsShortLink()) qs.questioncommon.SitemapCron(ctx) } diff --git a/internal/service/rank/rank_service.go b/internal/service/rank/rank_service.go index 307bc591..2bf222a4 100644 --- a/internal/service/rank/rank_service.go +++ b/internal/service/rank/rank_service.go @@ -29,6 +29,10 @@ const ( ) type UserRankRepo interface { + GetMaxDailyRank(ctx context.Context) (maxDailyRank int, err error) + CheckReachLimit(ctx context.Context, session *xorm.Session, userID string, maxDailyRank int) (reach bool, err error) + ChangeUserRank(ctx context.Context, session *xorm.Session, + userID string, userCurrentScore, deltaRank int) (err error) TriggerUserRank(ctx context.Context, session *xorm.Session, userId string, rank int, activityType int) (isReachStandard bool, err error) UserRankPage(ctx context.Context, userId string, page, pageSize int) (rankPage []*entity.Activity, total int64, err error) } diff --git a/internal/service/report_handle_admin/report_handle.go b/internal/service/report_handle_admin/report_handle.go index 65601aaa..bafe8729 100644 --- a/internal/service/report_handle_admin/report_handle.go +++ b/internal/service/report_handle_admin/report_handle.go @@ -4,30 +4,34 @@ import ( "context" "github.com/answerdev/answer/internal/service/config" + "github.com/answerdev/answer/internal/service/notice_queue" "github.com/answerdev/answer/internal/base/constant" "github.com/answerdev/answer/internal/entity" "github.com/answerdev/answer/internal/schema" "github.com/answerdev/answer/internal/service/comment" - "github.com/answerdev/answer/internal/service/notice_queue" questioncommon "github.com/answerdev/answer/internal/service/question_common" "github.com/answerdev/answer/pkg/obj" ) type ReportHandle struct { - questionCommon *questioncommon.QuestionCommon - commentRepo comment.CommentRepo - configService *config.ConfigService + questionCommon *questioncommon.QuestionCommon + commentRepo comment.CommentRepo + configService *config.ConfigService + notificationQueueService notice_queue.NotificationQueueService } func NewReportHandle( questionCommon *questioncommon.QuestionCommon, commentRepo comment.CommentRepo, - configService *config.ConfigService) *ReportHandle { + configService *config.ConfigService, + notificationQueueService notice_queue.NotificationQueueService, +) *ReportHandle { return &ReportHandle{ - questionCommon: questionCommon, - commentRepo: commentRepo, - configService: configService, + questionCommon: questionCommon, + commentRepo: commentRepo, + configService: configService, + notificationQueueService: notificationQueueService, } } @@ -88,5 +92,5 @@ func (rh *ReportHandle) sendNotification(ctx context.Context, reportedUserID, ob ObjectType: constant.ReportObjectType, NotificationAction: notificationAction, } - notice_queue.AddNotification(msg) + rh.notificationQueueService.Send(ctx, msg) } diff --git a/internal/service/revision_service.go b/internal/service/revision_service.go index 20d3c9ca..b8ea66d2 100644 --- a/internal/service/revision_service.go +++ b/internal/service/revision_service.go @@ -28,15 +28,17 @@ import ( // RevisionService user service type RevisionService struct { - revisionRepo revision.RevisionRepo - userCommon *usercommon.UserCommon - questionCommon *questioncommon.QuestionCommon - answerService *AnswerService - objectInfoService *object_info.ObjService - questionRepo questioncommon.QuestionRepo - answerRepo answercommon.AnswerRepo - tagRepo tag_common.TagRepo - tagCommon *tagcommon.TagCommonService + revisionRepo revision.RevisionRepo + userCommon *usercommon.UserCommon + questionCommon *questioncommon.QuestionCommon + answerService *AnswerService + objectInfoService *object_info.ObjService + questionRepo questioncommon.QuestionRepo + answerRepo answercommon.AnswerRepo + tagRepo tag_common.TagRepo + tagCommon *tagcommon.TagCommonService + notificationQueueService notice_queue.NotificationQueueService + activityQueueService activity_queue.ActivityQueueService } func NewRevisionService( @@ -49,17 +51,21 @@ func NewRevisionService( answerRepo answercommon.AnswerRepo, tagRepo tag_common.TagRepo, tagCommon *tagcommon.TagCommonService, + notificationQueueService notice_queue.NotificationQueueService, + activityQueueService activity_queue.ActivityQueueService, ) *RevisionService { return &RevisionService{ - revisionRepo: revisionRepo, - userCommon: userCommon, - questionCommon: questionCommon, - answerService: answerService, - objectInfoService: objectInfoService, - questionRepo: questionRepo, - answerRepo: answerRepo, - tagRepo: tagRepo, - tagCommon: tagCommon, + revisionRepo: revisionRepo, + userCommon: userCommon, + questionCommon: questionCommon, + answerService: answerService, + objectInfoService: objectInfoService, + questionRepo: questionRepo, + answerRepo: answerRepo, + tagRepo: tagRepo, + tagCommon: tagCommon, + notificationQueueService: notificationQueueService, + activityQueueService: activityQueueService, } } @@ -155,7 +161,7 @@ func (rs *RevisionService) revisionAuditQuestion(ctx context.Context, revisionit if saveerr != nil { return saveerr } - activity_queue.AddActivity(&schema.ActivityMsg{ + rs.activityQueueService.Send(ctx, &schema.ActivityMsg{ UserID: revisionitem.UserID, ObjectID: revisionitem.ObjectID, ActivityTypeKey: constant.ActQuestionEdited, @@ -210,9 +216,9 @@ func (rs *RevisionService) revisionAuditAnswer(ctx context.Context, revisionitem } msg.ObjectType = constant.AnswerObjectType msg.NotificationAction = constant.NotificationUpdateAnswer - notice_queue.AddNotification(msg) + rs.notificationQueueService.Send(ctx, msg) - activity_queue.AddActivity(&schema.ActivityMsg{ + rs.activityQueueService.Send(ctx, &schema.ActivityMsg{ UserID: revisionitem.UserID, ObjectID: insertData.ID, OriginalObjectID: insertData.ID, @@ -258,7 +264,7 @@ func (rs *RevisionService) revisionAuditTag(ctx context.Context, revisionitem *s } } - activity_queue.AddActivity(&schema.ActivityMsg{ + rs.activityQueueService.Send(ctx, &schema.ActivityMsg{ UserID: revisionitem.UserID, ObjectID: taginfo.TagID, OriginalObjectID: taginfo.TagID, diff --git a/internal/service/search_common/search.go b/internal/service/search_common/search.go index b9fc3041..ef7fe305 100644 --- a/internal/service/search_common/search.go +++ b/internal/service/search_common/search.go @@ -3,10 +3,12 @@ package search_common import ( "context" "github.com/answerdev/answer/internal/schema" + "github.com/answerdev/answer/plugin" ) type SearchRepo interface { SearchContents(ctx context.Context, words []string, tagIDs []string, userID string, votes, page, size int, order string) (resp []schema.SearchResp, total int64, err error) SearchQuestions(ctx context.Context, words []string, tagIDs []string, notAccepted bool, views, answers int, page, size int, order string) (resp []schema.SearchResp, total int64, err error) SearchAnswers(ctx context.Context, words []string, tagIDs []string, accepted bool, questionID string, page, size int, order string) (resp []schema.SearchResp, total int64, err error) + ParseSearchPluginResult(ctx context.Context, sres []plugin.SearchResult) (resp []schema.SearchResp, err error) } diff --git a/internal/service/search_parser/search_parser.go b/internal/service/search_parser/search_parser.go index ec56679f..1427fb44 100644 --- a/internal/service/search_parser/search_parser.go +++ b/internal/service/search_parser/search_parser.go @@ -2,6 +2,7 @@ package search_parser import ( "context" + "github.com/answerdev/answer/internal/base/constant" "regexp" "strings" @@ -25,144 +26,69 @@ func NewSearchParser(tagCommonService *tag_common.TagCommonService, userCommon * // ParseStructure parse search structure, maybe match one of type all/questions/answers, // but if match two type, it will return false -func (sp *SearchParser) ParseStructure(dto *schema.SearchDTO) ( - searchType string, - // search all - userID string, - votes int, - // search questions - notAccepted bool, - isQuestion bool, - views, - answers int, - // search answers - accepted bool, - questionID string, - isAnswer bool, - // common fields - tags, - words []string, -) { +func (sp *SearchParser) ParseStructure(ctx context.Context, dto *schema.SearchDTO) (cond *schema.SearchCondition) { + cond = &schema.SearchCondition{} var ( - query = dto.Query - currentUserID = dto.UserID - all = 0 - q = 0 - a = 0 - withWords []string - limitWords = 5 + query = dto.Query + limitWords = 5 ) // match tags - tags = sp.parseTags(&query) + cond.Tags = sp.parseTags(ctx, &query) // match all - userID = sp.parseUserID(&query, currentUserID) - if userID != "" { - searchType = "all" - all = 1 - } - votes = sp.parseVotes(&query) - if votes != -1 { - searchType = "all" - all = 1 - } - withWords = sp.parseWithin(&query) - if len(withWords) > 0 { - searchType = "all" - all = 1 - } + cond.UserID = sp.parseUserID(ctx, &query, dto.UserID) + cond.VoteAmount = sp.parseVotes(&query) + cond.Words = sp.parseWithin(&query) // match questions - notAccepted = sp.parseNotAccepted(&query) - if notAccepted { - searchType = "question" - q = 1 + cond.NotAccepted = sp.parseNotAccepted(&query) + if cond.NotAccepted { + cond.TargetType = constant.QuestionObjectType } - isQuestion = sp.parseIsQuestion(&query) - if isQuestion { - searchType = "question" - q = 1 + cond.Views = sp.parseViews(&query) + if cond.Views != -1 { + cond.TargetType = constant.QuestionObjectType } - views = sp.parseViews(&query) - if views != -1 { - searchType = "question" - q = 1 - } - answers = sp.parseAnswers(&query) - if answers != -1 { - searchType = "question" - q = 1 + cond.AnswerAmount = sp.parseAnswers(&query) + if cond.AnswerAmount != -1 { + cond.TargetType = constant.QuestionObjectType } // match answers - accepted = sp.parseAccepted(&query) - if accepted { - searchType = "answer" - a = 1 + cond.Accepted = sp.parseAccepted(&query) + if cond.Accepted { + cond.TargetType = constant.AnswerObjectType } - questionID = sp.parseQuestionID(&query) - if questionID != "" { - searchType = "answer" - a = 1 + cond.QuestionID = sp.parseQuestionID(&query) + if cond.QuestionID != "" { + cond.TargetType = constant.AnswerObjectType } - isAnswer = sp.parseIsAnswer(&query) - if isAnswer { - searchType = "answer" - a = 1 + + if sp.parseIsQuestion(&query) { + cond.TargetType = constant.QuestionObjectType + } + if sp.parseIsAnswer(&query) { + cond.TargetType = constant.AnswerObjectType } if len(strings.TrimSpace(query)) > 0 { - words = strings.Split(strings.TrimSpace(query), " ") - } else { - words = []string{} - } - - if len(withWords) > 0 { - words = append(withWords, words...) + words := strings.Split(strings.TrimSpace(query), " ") + cond.Words = append(cond.Words, words...) } // check limit words - if len(words) > limitWords { - words = words[:limitWords] + if len(cond.Words) > limitWords { + cond.Words = cond.Words[:limitWords] } - - // check tags' search is all or question - if len(tags) > 0 { - if len(words) > 0 { - searchType = "all" - all = 1 - } else if isAnswer { - searchType = "answer" - a = 1 - all = 0 - q = 0 - } else { - searchType = "question" - q = 1 - all = 0 - a = 0 - } - } - - // check match types greater than 1 - if all+q+a > 1 { - searchType = "" - } - - // check not match - if all+q+a == 0 && len(words) > 0 { - searchType = "all" - } - return } // parseTags parse search tags, return tag ids array -func (sp *SearchParser) parseTags(query *string) (tags []string) { +func (sp *SearchParser) parseTags(ctx context.Context, query *string) (tags []string) { var ( // expire tag pattern - exprTag = `(?m)\[([a-zA-Z0-9-\+\.#]+)\]{1}?` + exprTag = `\[(.*?)\]` q = *query limit = 5 ) @@ -175,7 +101,7 @@ func (sp *SearchParser) parseTags(query *string) (tags []string) { tags = []string{} for _, item := range res { - tag, exists, err := sp.tagCommonService.GetTagBySlugName(context.TODO(), item[1]) + tag, exists, err := sp.tagCommonService.GetTagBySlugName(ctx, item[1]) if err != nil || !exists { continue } @@ -193,21 +119,21 @@ func (sp *SearchParser) parseTags(query *string) (tags []string) { } // parseUserID return user id or current login user id -func (sp *SearchParser) parseUserID(query *string, currentUserID string) (userID string) { +func (sp *SearchParser) parseUserID(ctx context.Context, query *string, currentUserID string) (userID string) { var ( - exprUserID = `(?m)^user:([a-z0-9._-]+)` - exprMe = "user:me" - q = *query + exprUsername = `user:(\S+)` + exprMe = "user:me" + q = *query ) - re := regexp.MustCompile(exprUserID) + re := regexp.MustCompile(exprUsername) res := re.FindStringSubmatch(q) if strings.Contains(q, exprMe) { userID = currentUserID q = strings.ReplaceAll(q, exprMe, "") - } else if len(res) == 2 { + } else if len(res) > 1 { name := res[1] - user, has, err := sp.userCommon.GetUserBasicInfoByUserName(context.TODO(), name) + user, has, err := sp.userCommon.GetUserBasicInfoByUserName(ctx, name) if err == nil && has { userID = user.ID q = re.ReplaceAllString(q, "") @@ -220,14 +146,14 @@ func (sp *SearchParser) parseUserID(query *string, currentUserID string) (userID // parseVotes return the votes of search query func (sp *SearchParser) parseVotes(query *string) (votes int) { var ( - expr = `(?m)^score:([0-9]+)` + expr = `score:(\d+)` q = *query ) votes = -1 re := regexp.MustCompile(expr) res := re.FindStringSubmatch(q) - if len(res) == 2 { + if len(res) > 1 { votes = converter.StringToInt(res[1]) q = re.ReplaceAllString(q, "") } @@ -292,13 +218,13 @@ func (sp *SearchParser) parseIsQuestion(query *string) (isQuestion bool) { func (sp *SearchParser) parseViews(query *string) (views int) { var ( q = *query - expr = `(?m)^views:([0-9]+)` + expr = `views:(\d+)` ) views = -1 re := regexp.MustCompile(expr) res := re.FindStringSubmatch(q) - if len(res) == 2 { + if len(res) > 1 { views = converter.StringToInt(res[1]) q = re.ReplaceAllString(q, "") } @@ -310,13 +236,13 @@ func (sp *SearchParser) parseViews(query *string) (views int) { func (sp *SearchParser) parseAnswers(query *string) (answers int) { var ( q = *query - expr = `(?m)^answers:([0-9]+)` + expr = `answers:(\d+)` ) answers = -1 re := regexp.MustCompile(expr) res := re.FindStringSubmatch(q) - if len(res) == 2 { + if len(res) > 1 { answers = converter.StringToInt(res[1]) q = re.ReplaceAllString(q, "") } @@ -345,7 +271,7 @@ func (sp *SearchParser) parseAccepted(query *string) (accepted bool) { func (sp *SearchParser) parseQuestionID(query *string) (questionID string) { var ( q = *query - expr = `(?m)^inquestion:([0-9]+)` + expr = `inquestion:(\d+)` ) re := regexp.MustCompile(expr) diff --git a/internal/service/search_service.go b/internal/service/search_service.go index 66d762ab..995fe039 100644 --- a/internal/service/search_service.go +++ b/internal/service/search_service.go @@ -5,6 +5,7 @@ import ( "github.com/answerdev/answer/internal/schema" "github.com/answerdev/answer/internal/service/search_common" "github.com/answerdev/answer/internal/service/search_parser" + "github.com/answerdev/answer/plugin" ) type SearchService struct { @@ -23,40 +24,48 @@ func NewSearchService( } // Search search contents -func (ss *SearchService) Search(ctx context.Context, dto *schema.SearchDTO) (resp []schema.SearchResp, total int64, extra interface{}, err error) { - extra = nil +func (ss *SearchService) Search(ctx context.Context, dto *schema.SearchDTO) (resp []schema.SearchResp, total int64, err error) { if dto.Page < 1 { dto.Page = 1 } // search type - searchType, - // search all - userID, - votes, - // search questions - notAccepted, - _, - views, - answers, - // search answers - accepted, - questionID, - _, - // common fields - tags, - words := ss.searchParser.ParseStructure(dto) + cond := ss.searchParser.ParseStructure(ctx, dto) - switch searchType { - case "all": - resp, total, err = ss.searchRepo.SearchContents(ctx, words, tags, userID, votes, dto.Page, dto.Size, dto.Order) - if err != nil { - return nil, 0, nil, err + // check search plugin + var s plugin.Search + _ = plugin.CallSearch(func(search plugin.Search) error { + s = search + return nil + }) + + // search plugin is not found, call system search + if s == nil { + if cond.SearchAll() { + resp, total, err = ss.searchRepo.SearchContents(ctx, cond.Words, cond.Tags, cond.UserID, cond.VoteAmount, dto.Page, dto.Size, dto.Order) + } else if cond.SearchQuestion() { + resp, total, err = ss.searchRepo.SearchQuestions(ctx, cond.Words, cond.Tags, cond.NotAccepted, cond.Views, cond.AnswerAmount, dto.Page, dto.Size, dto.Order) + } else if cond.SearchAnswer() { + resp, total, err = ss.searchRepo.SearchAnswers(ctx, cond.Words, cond.Tags, cond.Accepted, cond.QuestionID, dto.Page, dto.Size, dto.Order) } - case "question": - resp, total, err = ss.searchRepo.SearchQuestions(ctx, words, tags, notAccepted, views, answers, dto.Page, dto.Size, dto.Order) - case "answer": - resp, total, err = ss.searchRepo.SearchAnswers(ctx, words, tags, accepted, questionID, dto.Page, dto.Size, dto.Order) + return + } + return ss.searchByPlugin(ctx, s, cond, dto) +} + +func (ss *SearchService) searchByPlugin(ctx context.Context, finder plugin.Search, cond *schema.SearchCondition, dto *schema.SearchDTO) (resp []schema.SearchResp, total int64, err error) { + var res []plugin.SearchResult + if cond.SearchAll() { + res, total, err = finder.SearchContents(ctx, cond.Convert2PluginSearchCond(dto.Page, dto.Size, dto.Order)) + } else if cond.SearchQuestion() { + res, total, err = finder.SearchQuestions(ctx, cond.Convert2PluginSearchCond(dto.Page, dto.Size, dto.Order)) + } else if cond.SearchAnswer() { + res, total, err = finder.SearchAnswers(ctx, cond.Convert2PluginSearchCond(dto.Page, dto.Size, dto.Order)) + } + + resp, err = ss.searchRepo.ParseSearchPluginResult(ctx, res) + if err != nil { + return nil, 0, err } return } diff --git a/internal/service/siteinfo/siteinfo_service.go b/internal/service/siteinfo/siteinfo_service.go index 5d0fc294..daf20e65 100644 --- a/internal/service/siteinfo/siteinfo_service.go +++ b/internal/service/siteinfo/siteinfo_service.go @@ -16,7 +16,6 @@ import ( questioncommon "github.com/answerdev/answer/internal/service/question_common" "github.com/answerdev/answer/internal/service/siteinfo_common" tagcommon "github.com/answerdev/answer/internal/service/tag_common" - "github.com/answerdev/answer/pkg/uid" "github.com/answerdev/answer/plugin" "github.com/jinzhu/copier" "github.com/segmentfault/pacman/errors" @@ -25,7 +24,7 @@ import ( type SiteInfoService struct { siteInfoRepo siteinfo_common.SiteInfoRepo - siteInfoCommonService *siteinfo_common.SiteInfoCommonService + siteInfoCommonService siteinfo_common.SiteInfoCommonService emailService *export.EmailService tagCommonService *tagcommon.TagCommonService configService *config.ConfigService @@ -34,7 +33,7 @@ type SiteInfoService struct { func NewSiteInfoService( siteInfoRepo siteinfo_common.SiteInfoRepo, - siteInfoCommonService *siteinfo_common.SiteInfoCommonService, + siteInfoCommonService siteinfo_common.SiteInfoCommonService, emailService *export.EmailService, tagCommonService *tagcommon.TagCommonService, configService *config.ConfigService, @@ -280,28 +279,12 @@ func (s *SiteInfoService) GetSeo(ctx context.Context) (resp *schema.SiteSeoReq, } func (s *SiteInfoService) SaveSeo(ctx context.Context, req schema.SiteSeoReq) (err error) { - var ( - siteType = constant.SiteTypeSeo - content []byte - ) - content, _ = json.Marshal(req) - + content, _ := json.Marshal(req) data := entity.SiteInfo{ - Type: siteType, + Type: constant.SiteTypeSeo, Content: string(content), } - - err = s.siteInfoRepo.SaveByType(ctx, siteType, &data) - if err != nil { - return - } - if req.PermaLink == schema.PermaLinkQuestionIDAndTitleByShortID || req.PermaLink == schema.PermaLinkQuestionIDByShortID { - uid.ShortIDSwitch = true - } else { - uid.ShortIDSwitch = false - } - s.questioncommon.SitemapCron(ctx) - return + return s.siteInfoRepo.SaveByType(ctx, constant.SiteTypeSeo, &data) } func (s *SiteInfoService) GetPrivilegesConfig(ctx context.Context) (resp *schema.GetPrivilegesConfigResp, err error) { @@ -339,13 +322,7 @@ func (s *SiteInfoService) translatePrivilegeOptions(ctx context.Context) (option } func (s *SiteInfoService) UpdatePrivilegesConfig(ctx context.Context, req *schema.UpdatePrivilegesConfigReq) (err error) { - var chooseOption *schema.PrivilegeOption - for _, option := range schema.DefaultPrivilegeOptions { - if option.Level == req.Level { - chooseOption = option - break - } - } + chooseOption := schema.DefaultPrivilegeOptions.Choose(req.Level) if chooseOption == nil { return nil } diff --git a/internal/service/siteinfo_common/siteinfo_service.go b/internal/service/siteinfo_common/siteinfo_service.go index fff82a8d..cf7a8be8 100644 --- a/internal/service/siteinfo_common/siteinfo_service.go +++ b/internal/service/siteinfo_common/siteinfo_service.go @@ -8,7 +8,6 @@ import ( "github.com/answerdev/answer/internal/entity" "github.com/answerdev/answer/internal/schema" "github.com/answerdev/answer/pkg/gravatar" - "github.com/answerdev/answer/pkg/uid" "github.com/segmentfault/pacman/log" ) @@ -18,31 +17,36 @@ type SiteInfoRepo interface { GetByType(ctx context.Context, siteType string) (siteInfo *entity.SiteInfo, exist bool, err error) } -// SiteInfoCommonService site info common service -type SiteInfoCommonService struct { +// siteInfoCommonService site info common service +type siteInfoCommonService struct { siteInfoRepo SiteInfoRepo } +type SiteInfoCommonService interface { + GetSiteGeneral(ctx context.Context) (resp *schema.SiteGeneralResp, err error) + GetSiteInterface(ctx context.Context) (resp *schema.SiteInterfaceResp, err error) + GetSiteBranding(ctx context.Context) (resp *schema.SiteBrandingResp, err error) + GetSiteUsers(ctx context.Context) (resp *schema.SiteUsersResp, err error) + FormatAvatar(ctx context.Context, originalAvatarData, email string) *schema.AvatarInfo + FormatListAvatar(ctx context.Context, userList []*entity.User) (userID2AvatarMapping map[string]*schema.AvatarInfo) + GetSiteWrite(ctx context.Context) (resp *schema.SiteWriteResp, err error) + GetSiteLegal(ctx context.Context) (resp *schema.SiteLegalResp, err error) + GetSiteLogin(ctx context.Context) (resp *schema.SiteLoginResp, err error) + GetSiteCustomCssHTML(ctx context.Context) (resp *schema.SiteCustomCssHTMLResp, err error) + GetSiteTheme(ctx context.Context) (resp *schema.SiteThemeResp, err error) + GetSiteSeo(ctx context.Context) (resp *schema.SiteSeoResp, err error) + GetSiteInfoByType(ctx context.Context, siteType string, resp interface{}) (err error) +} + // NewSiteInfoCommonService new site info common service -func NewSiteInfoCommonService(siteInfoRepo SiteInfoRepo) *SiteInfoCommonService { - siteInfo := &SiteInfoCommonService{ +func NewSiteInfoCommonService(siteInfoRepo SiteInfoRepo) SiteInfoCommonService { + return &siteInfoCommonService{ siteInfoRepo: siteInfoRepo, } - seoinfo, err := siteInfo.GetSiteSeo(context.Background()) - if err != nil { - log.Error("seoinfo error", err) - } - if seoinfo.PermaLink == schema.PermaLinkQuestionIDAndTitleByShortID || seoinfo.PermaLink == schema.PermaLinkQuestionIDByShortID { - uid.ShortIDSwitch = true - } else { - uid.ShortIDSwitch = false - } - - return siteInfo } // GetSiteGeneral get site info general -func (s *SiteInfoCommonService) GetSiteGeneral(ctx context.Context) (resp *schema.SiteGeneralResp, err error) { +func (s *siteInfoCommonService) GetSiteGeneral(ctx context.Context) (resp *schema.SiteGeneralResp, err error) { resp = &schema.SiteGeneralResp{} if err = s.GetSiteInfoByType(ctx, constant.SiteTypeGeneral, resp); err != nil { return nil, err @@ -51,7 +55,7 @@ func (s *SiteInfoCommonService) GetSiteGeneral(ctx context.Context) (resp *schem } // GetSiteInterface get site info interface -func (s *SiteInfoCommonService) GetSiteInterface(ctx context.Context) (resp *schema.SiteInterfaceResp, err error) { +func (s *siteInfoCommonService) GetSiteInterface(ctx context.Context) (resp *schema.SiteInterfaceResp, err error) { resp = &schema.SiteInterfaceResp{} if err = s.GetSiteInfoByType(ctx, constant.SiteTypeInterface, resp); err != nil { return nil, err @@ -60,7 +64,7 @@ func (s *SiteInfoCommonService) GetSiteInterface(ctx context.Context) (resp *sch } // GetSiteBranding get site info branding -func (s *SiteInfoCommonService) GetSiteBranding(ctx context.Context) (resp *schema.SiteBrandingResp, err error) { +func (s *siteInfoCommonService) GetSiteBranding(ctx context.Context) (resp *schema.SiteBrandingResp, err error) { resp = &schema.SiteBrandingResp{} if err = s.GetSiteInfoByType(ctx, constant.SiteTypeBranding, resp); err != nil { return nil, err @@ -69,7 +73,7 @@ func (s *SiteInfoCommonService) GetSiteBranding(ctx context.Context) (resp *sche } // GetSiteUsers get site info about users -func (s *SiteInfoCommonService) GetSiteUsers(ctx context.Context) (resp *schema.SiteUsersResp, err error) { +func (s *siteInfoCommonService) GetSiteUsers(ctx context.Context) (resp *schema.SiteUsersResp, err error) { resp = &schema.SiteUsersResp{} if err = s.GetSiteInfoByType(ctx, constant.SiteTypeUsers, resp); err != nil { return nil, err @@ -78,13 +82,13 @@ func (s *SiteInfoCommonService) GetSiteUsers(ctx context.Context) (resp *schema. } // FormatAvatar format avatar -func (s *SiteInfoCommonService) FormatAvatar(ctx context.Context, originalAvatarData, email string) *schema.AvatarInfo { +func (s *siteInfoCommonService) FormatAvatar(ctx context.Context, originalAvatarData, email string) *schema.AvatarInfo { gravatarBaseURL, defaultAvatar := s.getAvatarDefaultConfig(ctx) return s.selectedAvatar(originalAvatarData, defaultAvatar, gravatarBaseURL, email) } // FormatListAvatar format avatar -func (s *SiteInfoCommonService) FormatListAvatar(ctx context.Context, userList []*entity.User) ( +func (s *siteInfoCommonService) FormatListAvatar(ctx context.Context, userList []*entity.User) ( avatarMapping map[string]*schema.AvatarInfo) { gravatarBaseURL, defaultAvatar := s.getAvatarDefaultConfig(ctx) avatarMapping = make(map[string]*schema.AvatarInfo) @@ -94,19 +98,22 @@ func (s *SiteInfoCommonService) FormatListAvatar(ctx context.Context, userList [ return avatarMapping } -func (s *SiteInfoCommonService) getAvatarDefaultConfig(ctx context.Context) (string, string) { +func (s *siteInfoCommonService) getAvatarDefaultConfig(ctx context.Context) (string, string) { gravatarBaseURL, defaultAvatar := constant.DefaultGravatarBaseURL, constant.DefaultAvatar usersConfig, err := s.GetSiteUsers(ctx) if err != nil { log.Error(err) - } else { + } + if len(usersConfig.GravatarBaseURL) > 0 { gravatarBaseURL = usersConfig.GravatarBaseURL + } + if len(usersConfig.DefaultAvatar) > 0 { defaultAvatar = usersConfig.DefaultAvatar } return gravatarBaseURL, defaultAvatar } -func (s *SiteInfoCommonService) selectedAvatar( +func (s *siteInfoCommonService) selectedAvatar( originalAvatarData string, defaultAvatar string, gravatarBaseURL string, email string) *schema.AvatarInfo { avatarInfo := &schema.AvatarInfo{} _ = json.Unmarshal([]byte(originalAvatarData), avatarInfo) @@ -121,7 +128,7 @@ func (s *SiteInfoCommonService) selectedAvatar( } // GetSiteWrite get site info write -func (s *SiteInfoCommonService) GetSiteWrite(ctx context.Context) (resp *schema.SiteWriteResp, err error) { +func (s *siteInfoCommonService) GetSiteWrite(ctx context.Context) (resp *schema.SiteWriteResp, err error) { resp = &schema.SiteWriteResp{} if err = s.GetSiteInfoByType(ctx, constant.SiteTypeWrite, resp); err != nil { return nil, err @@ -130,7 +137,7 @@ func (s *SiteInfoCommonService) GetSiteWrite(ctx context.Context) (resp *schema. } // GetSiteLegal get site info write -func (s *SiteInfoCommonService) GetSiteLegal(ctx context.Context) (resp *schema.SiteLegalResp, err error) { +func (s *siteInfoCommonService) GetSiteLegal(ctx context.Context) (resp *schema.SiteLegalResp, err error) { resp = &schema.SiteLegalResp{} if err = s.GetSiteInfoByType(ctx, constant.SiteTypeLegal, resp); err != nil { return nil, err @@ -139,7 +146,7 @@ func (s *SiteInfoCommonService) GetSiteLegal(ctx context.Context) (resp *schema. } // GetSiteLogin get site login config -func (s *SiteInfoCommonService) GetSiteLogin(ctx context.Context) (resp *schema.SiteLoginResp, err error) { +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 @@ -148,7 +155,7 @@ func (s *SiteInfoCommonService) GetSiteLogin(ctx context.Context) (resp *schema. } // GetSiteCustomCssHTML get site custom css html config -func (s *SiteInfoCommonService) GetSiteCustomCssHTML(ctx context.Context) (resp *schema.SiteCustomCssHTMLResp, err error) { +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 @@ -157,7 +164,7 @@ func (s *SiteInfoCommonService) GetSiteCustomCssHTML(ctx context.Context) (resp } // GetSiteTheme get site theme -func (s *SiteInfoCommonService) GetSiteTheme(ctx context.Context) (resp *schema.SiteThemeResp, err error) { +func (s *siteInfoCommonService) GetSiteTheme(ctx context.Context) (resp *schema.SiteThemeResp, err error) { resp = &schema.SiteThemeResp{ ThemeOptions: schema.GetThemeOptions, } @@ -169,15 +176,24 @@ func (s *SiteInfoCommonService) GetSiteTheme(ctx context.Context) (resp *schema. } // GetSiteSeo get site seo -func (s *SiteInfoCommonService) GetSiteSeo(ctx context.Context) (resp *schema.SiteSeoReq, err error) { - resp = &schema.SiteSeoReq{} +func (s *siteInfoCommonService) GetSiteSeo(ctx context.Context) (resp *schema.SiteSeoResp, err error) { + resp = &schema.SiteSeoResp{} 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) { +func (s *siteInfoCommonService) EnableShortID(ctx context.Context) (enabled bool) { + siteSeo, err := s.GetSiteSeo(ctx) + if err != nil { + log.Error(err) + return false + } + return siteSeo.IsShortLink() +} + +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 diff --git a/internal/service/tag/tag_service.go b/internal/service/tag/tag_service.go index 23dc25de..6d2884a6 100644 --- a/internal/service/tag/tag_service.go +++ b/internal/service/tag/tag_service.go @@ -10,6 +10,7 @@ import ( "github.com/answerdev/answer/internal/service/siteinfo_common" tagcommonser "github.com/answerdev/answer/internal/service/tag_common" "github.com/answerdev/answer/pkg/htmltext" + "github.com/jinzhu/copier" "github.com/answerdev/answer/internal/base/pager" "github.com/answerdev/answer/internal/base/reason" @@ -18,18 +19,18 @@ import ( "github.com/answerdev/answer/internal/service/activity_common" "github.com/answerdev/answer/internal/service/permission" "github.com/answerdev/answer/pkg/converter" - "github.com/jinzhu/copier" "github.com/segmentfault/pacman/errors" "github.com/segmentfault/pacman/log" ) // TagService user service type TagService struct { - tagRepo tagcommonser.TagRepo - tagCommonService *tagcommonser.TagCommonService - revisionService *revision_common.RevisionService - followCommon activity_common.FollowRepo - siteInfoService *siteinfo_common.SiteInfoCommonService + tagRepo tagcommonser.TagRepo + tagCommonService *tagcommonser.TagCommonService + revisionService *revision_common.RevisionService + followCommon activity_common.FollowRepo + siteInfoService siteinfo_common.SiteInfoCommonService + activityQueueService activity_queue.ActivityQueueService } // NewTagService new tag service @@ -38,13 +39,16 @@ func NewTagService( tagCommonService *tagcommonser.TagCommonService, revisionService *revision_common.RevisionService, followCommon activity_common.FollowRepo, - siteInfoService *siteinfo_common.SiteInfoCommonService) *TagService { + siteInfoService siteinfo_common.SiteInfoCommonService, + activityQueueService activity_queue.ActivityQueueService, +) *TagService { return &TagService{ - tagRepo: tagRepo, - tagCommonService: tagCommonService, - revisionService: revisionService, - followCommon: followCommon, - siteInfoService: siteInfoService, + tagRepo: tagRepo, + tagCommonService: tagCommonService, + revisionService: revisionService, + followCommon: followCommon, + siteInfoService: siteInfoService, + activityQueueService: activityQueueService, } } @@ -73,7 +77,7 @@ func (ts *TagService) RemoveTag(ctx context.Context, req *schema.RemoveTagReq) ( if err != nil { return err } - activity_queue.AddActivity(&schema.ActivityMsg{ + ts.activityQueueService.Send(ctx, &schema.ActivityMsg{ UserID: req.UserID, ObjectID: req.TagID, OriginalObjectID: req.TagID, @@ -298,7 +302,7 @@ func (ts *TagService) UpdateTagSynonym(ctx context.Context, req *schema.UpdateTa if err != nil { return err } - activity_queue.AddActivity(&schema.ActivityMsg{ + ts.activityQueueService.Send(ctx, &schema.ActivityMsg{ UserID: req.UserID, ObjectID: tag.ID, OriginalObjectID: tag.ID, diff --git a/internal/service/tag_common/tag_common.go b/internal/service/tag_common/tag_common.go index 7332ab80..b57eb240 100644 --- a/internal/service/tag_common/tag_common.go +++ b/internal/service/tag_common/tag_common.go @@ -57,11 +57,12 @@ type TagRelRepo interface { // TagCommonService user service type TagCommonService struct { - revisionService *revision_common.RevisionService - tagCommonRepo TagCommonRepo - tagRelRepo TagRelRepo - tagRepo TagRepo - siteInfoService *siteinfo_common.SiteInfoCommonService + revisionService *revision_common.RevisionService + tagCommonRepo TagCommonRepo + tagRelRepo TagRelRepo + tagRepo TagRepo + siteInfoService siteinfo_common.SiteInfoCommonService + activityQueueService activity_queue.ActivityQueueService } // NewTagCommonService new tag service @@ -70,14 +71,16 @@ func NewTagCommonService( tagRelRepo TagRelRepo, tagRepo TagRepo, revisionService *revision_common.RevisionService, - siteInfoService *siteinfo_common.SiteInfoCommonService, + siteInfoService siteinfo_common.SiteInfoCommonService, + activityQueueService activity_queue.ActivityQueueService, ) *TagCommonService { return &TagCommonService{ - tagCommonRepo: tagCommonRepo, - tagRelRepo: tagRelRepo, - tagRepo: tagRepo, - revisionService: revisionService, - siteInfoService: siteInfoService, + tagCommonRepo: tagCommonRepo, + tagRelRepo: tagRelRepo, + tagRepo: tagRepo, + revisionService: revisionService, + siteInfoService: siteInfoService, + activityQueueService: activityQueueService, } } @@ -645,7 +648,7 @@ func (ts *TagCommonService) ObjectChangeTag(ctx context.Context, objectTagData * if err != nil { return err } - activity_queue.AddActivity(&schema.ActivityMsg{ + ts.activityQueueService.Send(ctx, &schema.ActivityMsg{ UserID: objectTagData.UserID, ObjectID: tag.ID, OriginalObjectID: tag.ID, @@ -845,7 +848,7 @@ func (ts *TagCommonService) UpdateTag(ctx context.Context, req *schema.UpdateTag return err } if canUpdate { - activity_queue.AddActivity(&schema.ActivityMsg{ + ts.activityQueueService.Send(ctx, &schema.ActivityMsg{ UserID: req.UserID, ObjectID: tagInfo.ID, OriginalObjectID: tagInfo.ID, diff --git a/internal/service/uploader/upload.go b/internal/service/uploader/upload.go index 3b978521..f6c14686 100644 --- a/internal/service/uploader/upload.go +++ b/internal/service/uploader/upload.go @@ -53,20 +53,20 @@ var ( type UploaderService interface { UploadAvatarFile(ctx *gin.Context) (url string, err error) - AvatarThumbFile(ctx *gin.Context, uploadPath, fileName string, size int) (avatarFile []byte, err error) UploadPostFile(ctx *gin.Context) (url string, err error) UploadBrandingFile(ctx *gin.Context) (url string, err error) + AvatarThumbFile(ctx *gin.Context, fileName string, size int) (url string, err error) } // uploaderService uploader service type uploaderService struct { serviceConfig *service_config.ServiceConfig - siteInfoService *siteinfo_common.SiteInfoCommonService + siteInfoService siteinfo_common.SiteInfoCommonService } // NewUploaderService new upload service func NewUploaderService(serviceConfig *service_config.ServiceConfig, - siteInfoService *siteinfo_common.SiteInfoCommonService) UploaderService { + siteInfoService siteinfo_common.SiteInfoCommonService) UploaderService { for _, subPath := range subPathList { err := dir.CreateDirIfNotExist(filepath.Join(serviceConfig.UploadPath, subPath)) if err != nil { @@ -105,26 +105,26 @@ func (us *uploaderService) UploadAvatarFile(ctx *gin.Context) (url string, err e return us.uploadFile(ctx, file, avatarFilePath) } -func (us *uploaderService) AvatarThumbFile(ctx *gin.Context, uploadPath, fileName string, size int) ( - avatarfile []byte, err error) { +func (us *uploaderService) AvatarThumbFile(ctx *gin.Context, fileName string, size int) (url string, err error) { if size > 1024 { size = 1024 } + thumbFileName := fmt.Sprintf("%d_%d@%s", size, size, fileName) - thumbfilePath := fmt.Sprintf("%s/%s/%s", uploadPath, avatarThumbSubPath, thumbFileName) - avatarfile, err = os.ReadFile(thumbfilePath) + thumbFilePath := fmt.Sprintf("%s/%s/%s", us.serviceConfig.UploadPath, avatarThumbSubPath, thumbFileName) + avatarfile, err := os.ReadFile(thumbFilePath) if err == nil { - return avatarfile, nil + return thumbFilePath, nil } - filePath := fmt.Sprintf("%s/avatar/%s", uploadPath, fileName) + filePath := fmt.Sprintf("%s/avatar/%s", us.serviceConfig.UploadPath, fileName) avatarfile, err = os.ReadFile(filePath) if err != nil { - return avatarfile, errors.InternalServer(reason.UnknownError).WithError(err).WithStack() + return "", errors.InternalServer(reason.UnknownError).WithError(err).WithStack() } reader := bytes.NewReader(avatarfile) img, err := imaging.Decode(reader) if err != nil { - return avatarfile, errors.InternalServer(reason.UnknownError).WithError(err).WithStack() + return "", errors.InternalServer(reason.UnknownError).WithError(err).WithStack() } new_image := imaging.Fill(img, size, size, imaging.Center, imaging.Linear) var buf bytes.Buffer @@ -133,29 +133,29 @@ func (us *uploaderService) AvatarThumbFile(ctx *gin.Context, uploadPath, fileNam _, ok := FormatExts[fileSuffix] if !ok { - return avatarfile, fmt.Errorf("img extension not exist") + return "", 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() + return "", errors.InternalServer(reason.UnknownError).WithError(err).WithStack() } thumbReader := bytes.NewReader(buf.Bytes()) err = dir.CreateDirIfNotExist(path.Join(us.serviceConfig.UploadPath, avatarThumbSubPath)) if err != nil { - return nil, errors.InternalServer(reason.UnknownError).WithError(err).WithStack() + return "", errors.InternalServer(reason.UnknownError).WithError(err).WithStack() } avatarFilePath := path.Join(avatarThumbSubPath, thumbFileName) savefilePath := path.Join(us.serviceConfig.UploadPath, avatarFilePath) out, err := os.Create(savefilePath) if err != nil { - return avatarfile, errors.InternalServer(reason.UnknownError).WithError(err).WithStack() + return "", errors.InternalServer(reason.UnknownError).WithError(err).WithStack() } defer out.Close() _, err = io.Copy(out, thumbReader) if err != nil { - return avatarfile, errors.InternalServer(reason.UnknownError).WithError(err).WithStack() + return "", errors.InternalServer(reason.UnknownError).WithError(err).WithStack() } - return buf.Bytes(), nil + return savefilePath, nil } func (us *uploaderService) UploadPostFile(ctx *gin.Context) ( diff --git a/internal/service/user_admin/user_backyard.go b/internal/service/user_admin/user_backyard.go index 86270e08..3de19dce 100644 --- a/internal/service/user_admin/user_backyard.go +++ b/internal/service/user_admin/user_backyard.go @@ -3,6 +3,8 @@ package user_admin import ( "context" "fmt" + "github.com/answerdev/answer/internal/service/export" + "github.com/google/uuid" "net/mail" "strings" "time" @@ -41,7 +43,8 @@ type UserAdminService struct { authService *auth.AuthService userCommonService *usercommon.UserCommon userActivity activity.UserActiveActivityRepo - siteInfoCommonService *siteinfo_common.SiteInfoCommonService + siteInfoCommonService siteinfo_common.SiteInfoCommonService + emailService *export.EmailService } // NewUserAdminService new user admin service @@ -51,7 +54,8 @@ func NewUserAdminService( authService *auth.AuthService, userCommonService *usercommon.UserCommon, userActivity activity.UserActiveActivityRepo, - siteInfoCommonService *siteinfo_common.SiteInfoCommonService, + siteInfoCommonService siteinfo_common.SiteInfoCommonService, + emailService *export.EmailService, ) *UserAdminService { return &UserAdminService{ userRepo: userRepo, @@ -60,6 +64,7 @@ func NewUserAdminService( userCommonService: userCommonService, userActivity: userActivity, siteInfoCommonService: siteInfoCommonService, + emailService: emailService, } } @@ -293,3 +298,61 @@ func (us *UserAdminService) setUserRoleInfo(ctx context.Context, resp []*schema. u.RoleName = r.Name } } + +func (us *UserAdminService) GetUserActivation(ctx context.Context, req *schema.GetUserActivationReq) ( + resp *schema.GetUserActivationResp, err error) { + user, exist, err := us.userRepo.GetUserInfo(ctx, req.UserID) + if err != nil { + return nil, err + } + if !exist { + return nil, errors.BadRequest(reason.UserNotFound) + } + + general, err := us.siteInfoCommonService.GetSiteGeneral(ctx) + if err != nil { + return nil, err + } + + data := &schema.EmailCodeContent{ + Email: user.EMail, + UserID: user.ID, + } + code := uuid.NewString() + us.emailService.SaveCode(ctx, code, data.ToJSONString()) + resp = &schema.GetUserActivationResp{ + ActivationURL: fmt.Sprintf("%s/users/account-activation?code=%s", general.SiteUrl, code), + } + return resp, nil +} + +// SendUserActivation send user activation email +func (us *UserAdminService) SendUserActivation(ctx context.Context, req *schema.SendUserActivationReq) (err error) { + user, exist, err := us.userRepo.GetUserInfo(ctx, req.UserID) + if err != nil { + return err + } + if !exist { + return errors.BadRequest(reason.UserNotFound) + } + + general, err := us.siteInfoCommonService.GetSiteGeneral(ctx) + if err != nil { + return err + } + + data := &schema.EmailCodeContent{ + Email: user.EMail, + UserID: user.ID, + } + code := uuid.NewString() + us.emailService.SaveCode(ctx, code, data.ToJSONString()) + + verifyEmailURL := fmt.Sprintf("%s/users/account-activation?code=%s", general.SiteUrl, code) + title, body, err := us.emailService.RegisterTemplate(ctx, verifyEmailURL) + if err != nil { + return err + } + go us.emailService.SendAndSaveCode(ctx, user.EMail, title, body, code, data.ToJSONString()) + return nil +} diff --git a/internal/service/user_common/user.go b/internal/service/user_common/user.go index 9a451265..0b419d4a 100644 --- a/internal/service/user_common/user.go +++ b/internal/service/user_common/user.go @@ -36,7 +36,7 @@ type UserRepo interface { GetByUsernames(ctx context.Context, usernames []string) ([]*entity.User, error) GetByEmail(ctx context.Context, email string) (userInfo *entity.User, exist bool, err error) GetUserCount(ctx context.Context) (count int64, err error) - SearchUserListByName(ctx context.Context, name string) (userList []*entity.User, err error) + SearchUserListByName(ctx context.Context, name string, limit int) (userList []*entity.User, err error) } // UserCommon user service @@ -44,14 +44,14 @@ type UserCommon struct { userRepo UserRepo userRoleService *role.UserRoleRelService authService *auth.AuthService - siteInfoCommonService *siteinfo_common.SiteInfoCommonService + siteInfoCommonService siteinfo_common.SiteInfoCommonService } func NewUserCommon( userRepo UserRepo, userRoleService *role.UserRoleRelService, authService *auth.AuthService, - siteInfoCommonService *siteinfo_common.SiteInfoCommonService, + siteInfoCommonService siteinfo_common.SiteInfoCommonService, ) *UserCommon { return &UserCommon{ userRepo: userRepo, @@ -151,7 +151,7 @@ func (us *UserCommon) MakeUsername(ctx context.Context, displayName string) (use } } - username = strings.ReplaceAll(displayName, " ", "_") + username = strings.ReplaceAll(displayName, " ", "-") username = strings.ToLower(username) suffix := "" diff --git a/internal/service/user_external_login/user_center_login_service.go b/internal/service/user_external_login/user_center_login_service.go index c05b8927..58369d51 100644 --- a/internal/service/user_external_login/user_center_login_service.go +++ b/internal/service/user_external_login/user_center_login_service.go @@ -27,7 +27,7 @@ type UserCenterLoginService struct { userExternalLoginRepo UserExternalLoginRepo userCommonService *usercommon.UserCommon userActivity activity.UserActiveActivityRepo - siteInfoCommonService *siteinfo_common.SiteInfoCommonService + siteInfoCommonService siteinfo_common.SiteInfoCommonService } // NewUserCenterLoginService new user external login service @@ -36,7 +36,7 @@ func NewUserCenterLoginService( userCommonService *usercommon.UserCommon, userExternalLoginRepo UserExternalLoginRepo, userActivity activity.UserActiveActivityRepo, - siteInfoCommonService *siteinfo_common.SiteInfoCommonService, + siteInfoCommonService siteinfo_common.SiteInfoCommonService, ) *UserCenterLoginService { return &UserCenterLoginService{ userRepo: userRepo, @@ -156,7 +156,9 @@ func (us *UserCenterLoginService) registerNewUser(ctx context.Context, provider MetaInfo: string(metaInfo), } err = us.userExternalLoginRepo.AddUserExternalLogin(ctx, newExternalUserInfo) - + if err != nil { + return nil, err + } return userInfo, nil } diff --git a/internal/service/user_external_login/user_external_login_service.go b/internal/service/user_external_login/user_external_login_service.go index 4b10aef4..7e439ba3 100644 --- a/internal/service/user_external_login/user_external_login_service.go +++ b/internal/service/user_external_login/user_external_login_service.go @@ -41,7 +41,7 @@ type UserExternalLoginService struct { userExternalLoginRepo UserExternalLoginRepo userCommonService *usercommon.UserCommon emailService *export.EmailService - siteInfoCommonService *siteinfo_common.SiteInfoCommonService + siteInfoCommonService siteinfo_common.SiteInfoCommonService userActivity activity.UserActiveActivityRepo } @@ -51,7 +51,7 @@ func NewUserExternalLoginService( userCommonService *usercommon.UserCommon, userExternalLoginRepo UserExternalLoginRepo, emailService *export.EmailService, - siteInfoCommonService *siteinfo_common.SiteInfoCommonService, + siteInfoCommonService siteinfo_common.SiteInfoCommonService, userActivity activity.UserActiveActivityRepo, ) *UserExternalLoginService { return &UserExternalLoginService{ diff --git a/internal/service/user_service.go b/internal/service/user_service.go index 72b4440b..17d52da3 100644 --- a/internal/service/user_service.go +++ b/internal/service/user_service.go @@ -38,7 +38,7 @@ type UserService struct { activityRepo activity_common.ActivityRepo emailService *export.EmailService authService *auth.AuthService - siteInfoService *siteinfo_common.SiteInfoCommonService + siteInfoService siteinfo_common.SiteInfoCommonService userRoleService *role.UserRoleRelService userExternalLoginService *user_external_login.UserExternalLoginService } @@ -48,7 +48,7 @@ func NewUserService(userRepo usercommon.UserRepo, activityRepo activity_common.ActivityRepo, emailService *export.EmailService, authService *auth.AuthService, - siteInfoService *siteinfo_common.SiteInfoCommonService, + siteInfoService siteinfo_common.SiteInfoCommonService, userRoleService *role.UserRoleRelService, userCommonService *usercommon.UserCommon, userExternalLoginService *user_external_login.UserExternalLoginService, @@ -606,7 +606,7 @@ func (us *UserService) UserChangeEmailSendCode(ctx context.Context, req *schema. } log.Infof("send email confirmation %s", verifyEmailURL) - go us.emailService.SendAndSaveCode(context.Background(), req.Email, title, body, code, data.ToJSONString()) + go us.emailService.SendAndSaveCode(ctx, req.Email, title, body, code, data.ToJSONString()) return nil, nil } @@ -817,22 +817,26 @@ func (us *UserService) getUserInfoMapping(ctx context.Context, userIDs []string) return userInfoMapping, nil } -func (us *UserService) SearchUserListByName(ctx context.Context, input *schema.GetOtherUserInfoByUsernameReq) ([]*schema.UserBasicInfo, error) { - userinfolist := make([]*schema.UserBasicInfo, 0) - list, err := us.userRepo.SearchUserListByName(ctx, input.Username) +func (us *UserService) SearchUserListByName(ctx context.Context, req *schema.GetOtherUserInfoByUsernameReq) ( + resp []*schema.UserBasicInfo, err error) { + resp = make([]*schema.UserBasicInfo, 0) + if len(req.Username) == 0 { + return resp, nil + } + userList, err := us.userRepo.SearchUserListByName(ctx, req.Username, 5) if err != nil { - return userinfolist, err + return resp, err } - avatarMapping := us.siteInfoService.FormatListAvatar(ctx, list) - for _, user := range list { - if input.UserID != user.ID { - userinfo := us.userCommonService.FormatUserBasicInfo(ctx, user) - userinfo.Avatar = avatarMapping[user.ID].GetURL() - userinfolist = append(userinfolist, userinfo) + avatarMapping := us.siteInfoService.FormatListAvatar(ctx, userList) + for _, u := range userList { + if req.UserID == u.ID { + continue } - + basicInfo := us.userCommonService.FormatUserBasicInfo(ctx, u) + basicInfo.Avatar = avatarMapping[u.ID].GetURL() + resp = append(resp, basicInfo) } - return userinfolist, nil + return resp, nil } func (us *UserService) warpStatRankingResp( diff --git a/internal/service/vote_service.go b/internal/service/vote_service.go index 022f9dca..54fd432c 100644 --- a/internal/service/vote_service.go +++ b/internal/service/vote_service.go @@ -2,6 +2,8 @@ package service import ( "context" + "github.com/answerdev/answer/internal/service/activity_common" + "strings" "github.com/answerdev/answer/internal/base/constant" "github.com/answerdev/answer/internal/base/handler" @@ -13,41 +15,37 @@ import ( "github.com/answerdev/answer/internal/service/config" "github.com/answerdev/answer/internal/service/object_info" "github.com/answerdev/answer/pkg/htmltext" - "github.com/answerdev/answer/pkg/obj" "github.com/segmentfault/pacman/log" "github.com/answerdev/answer/internal/base/reason" "github.com/answerdev/answer/internal/schema" answercommon "github.com/answerdev/answer/internal/service/answer_common" questioncommon "github.com/answerdev/answer/internal/service/question_common" - "github.com/answerdev/answer/internal/service/unique" "github.com/segmentfault/pacman/errors" ) // VoteRepo activity repository type VoteRepo interface { - VoteUp(ctx context.Context, objectID string, userID, objectUserID string) (resp *schema.VoteResp, err error) - VoteDown(ctx context.Context, objectID string, userID, objectUserID string) (resp *schema.VoteResp, err error) - VoteUpCancel(ctx context.Context, objectID string, userID, objectUserID string) (resp *schema.VoteResp, err error) - VoteDownCancel(ctx context.Context, objectID string, userID, objectUserID string) (resp *schema.VoteResp, err error) - GetVoteResultByObjectId(ctx context.Context, objectID string) (resp *schema.VoteResp, err error) - ListUserVotes(ctx context.Context, userID string, req schema.GetVoteWithPageReq, activityTypes []int) (voteList []entity.Activity, total int64, err error) + Vote(ctx context.Context, op *schema.VoteOperationInfo) (err error) + CancelVote(ctx context.Context, op *schema.VoteOperationInfo) (err error) + GetAndSaveVoteResult(ctx context.Context, objectID, objectType string) (up, down int64, err error) + ListUserVotes(ctx context.Context, userID string, page int, pageSize int, activityTypes []int) ( + voteList []*entity.Activity, total int64, err error) } // VoteService user service type VoteService struct { voteRepo VoteRepo - UniqueIDRepo unique.UniqueIDRepo configService *config.ConfigService questionRepo questioncommon.QuestionRepo answerRepo answercommon.AnswerRepo commentCommonRepo comment_common.CommentCommonRepo objectService *object_info.ObjService + activityRepo activity_common.ActivityRepo } func NewVoteService( - VoteRepo VoteRepo, - uniqueIDRepo unique.UniqueIDRepo, + voteRepo VoteRepo, configService *config.ConfigService, questionRepo questioncommon.QuestionRepo, answerRepo answercommon.AnswerRepo, @@ -55,8 +53,7 @@ func NewVoteService( objectService *object_info.ObjService, ) *VoteService { return &VoteService{ - voteRepo: VoteRepo, - UniqueIDRepo: uniqueIDRepo, + voteRepo: voteRepo, configService: configService, questionRepo: questionRepo, answerRepo: answerRepo, @@ -66,94 +63,90 @@ func NewVoteService( } // VoteUp vote up -func (vs *VoteService) VoteUp(ctx context.Context, dto *schema.VoteDTO) (voteResp *schema.VoteResp, err error) { - voteResp = &schema.VoteResp{} - - var objectUserID string - - objectUserID, err = vs.GetObjectUserID(ctx, dto.ObjectID) +func (vs *VoteService) VoteUp(ctx context.Context, req *schema.VoteReq) (resp *schema.VoteResp, err error) { + objectInfo, err := vs.objectService.GetInfo(ctx, req.ObjectID) if err != nil { - return + return nil, err } + // make object id must be decoded + objectInfo.ObjectID = req.ObjectID // check user is voting self or not - if objectUserID == dto.UserID { - err = errors.BadRequest(reason.DisallowVoteYourSelf) - return + if objectInfo.ObjectCreatorUserID == req.UserID { + return nil, errors.BadRequest(reason.DisallowVoteYourSelf) } - if dto.IsCancel { - return vs.voteRepo.VoteUpCancel(ctx, dto.ObjectID, dto.UserID, objectUserID) + voteUpOperationInfo := vs.createVoteOperationInfo(ctx, req.UserID, true, objectInfo) + + // vote operation + if req.IsCancel { + err = vs.voteRepo.CancelVote(ctx, voteUpOperationInfo) } else { - return vs.voteRepo.VoteUp(ctx, dto.ObjectID, dto.UserID, objectUserID) + // cancel vote down if exist + voteOperationInfo := vs.createVoteOperationInfo(ctx, req.UserID, false, objectInfo) + err = vs.voteRepo.CancelVote(ctx, voteOperationInfo) + if err != nil { + return nil, err + } + err = vs.voteRepo.Vote(ctx, voteUpOperationInfo) } + if err != nil { + return nil, err + } + + resp = &schema.VoteResp{} + resp.UpVotes, resp.DownVotes, err = vs.voteRepo.GetAndSaveVoteResult(ctx, req.ObjectID, objectInfo.ObjectType) + if err != nil { + log.Error(err) + } + resp.Votes = resp.UpVotes - resp.DownVotes + if !req.IsCancel { + resp.VoteStatus = constant.ActVoteUp + } + return resp, nil } // VoteDown vote down -func (vs *VoteService) VoteDown(ctx context.Context, dto *schema.VoteDTO) (voteResp *schema.VoteResp, err error) { - voteResp = &schema.VoteResp{} - - var objectUserID string - - objectUserID, err = vs.GetObjectUserID(ctx, dto.ObjectID) +func (vs *VoteService) VoteDown(ctx context.Context, req *schema.VoteReq) (resp *schema.VoteResp, err error) { + objectInfo, err := vs.objectService.GetInfo(ctx, req.ObjectID) if err != nil { - return + return nil, err } + // make object id must be decoded + objectInfo.ObjectID = req.ObjectID // check user is voting self or not - if objectUserID == dto.UserID { - err = errors.BadRequest(reason.DisallowVoteYourSelf) - return + if objectInfo.ObjectCreatorUserID == req.UserID { + return nil, errors.BadRequest(reason.DisallowVoteYourSelf) } - if dto.IsCancel { - return vs.voteRepo.VoteDownCancel(ctx, dto.ObjectID, dto.UserID, objectUserID) + // vote operation + voteDownOperationInfo := vs.createVoteOperationInfo(ctx, req.UserID, false, objectInfo) + if req.IsCancel { + err = vs.voteRepo.CancelVote(ctx, voteDownOperationInfo) } else { - return vs.voteRepo.VoteDown(ctx, dto.ObjectID, dto.UserID, objectUserID) + // cancel vote up if exist + err = vs.voteRepo.CancelVote(ctx, vs.createVoteOperationInfo(ctx, req.UserID, true, objectInfo)) + if err != nil { + return nil, err + } + err = vs.voteRepo.Vote(ctx, voteDownOperationInfo) } -} - -func (vs *VoteService) GetObjectUserID(ctx context.Context, objectID string) (userID string, err error) { - var objectKey string - objectKey, err = obj.GetObjectTypeStrByObjectID(objectID) + resp = &schema.VoteResp{} + resp.UpVotes, resp.DownVotes, err = vs.voteRepo.GetAndSaveVoteResult(ctx, req.ObjectID, objectInfo.ObjectType) if err != nil { - err = nil - return + log.Error(err) } - - switch objectKey { - case "question": - object, has, e := vs.questionRepo.GetQuestion(ctx, objectID) - if e != nil || !has { - err = errors.BadRequest(reason.QuestionNotFound).WithError(e).WithStack() - return - } - userID = object.UserID - case "answer": - object, has, e := vs.answerRepo.GetAnswer(ctx, objectID) - if e != nil || !has { - err = errors.BadRequest(reason.AnswerNotFound).WithError(e).WithStack() - return - } - userID = object.UserID - case "comment": - object, has, e := vs.commentCommonRepo.GetComment(ctx, objectID) - if e != nil || !has { - err = errors.BadRequest(reason.CommentNotFound).WithError(e).WithStack() - return - } - userID = object.UserID - default: - err = errors.BadRequest(reason.DisallowVote).WithError(err).WithStack() - return + resp.Votes = resp.UpVotes - resp.DownVotes + if !req.IsCancel { + resp.VoteStatus = constant.ActVoteDown } - - return + return resp, nil } // ListUserVotes list user's votes -func (vs *VoteService) ListUserVotes(ctx context.Context, req schema.GetVoteWithPageReq) (model *pager.PageModel, err error) { +func (vs *VoteService) ListUserVotes(ctx context.Context, req schema.GetVoteWithPageReq) (resp *pager.PageModel, err error) { typeKeys := []string{ activity_type.QuestionVoteUp, activity_type.QuestionVoteDown, @@ -172,14 +165,14 @@ func (vs *VoteService) ListUserVotes(ctx context.Context, req schema.GetVoteWith activityTypeMapping[cfg.ID] = typeKey } - voteList, total, err := vs.voteRepo.ListUserVotes(ctx, req.UserID, req, activityTypes) + voteList, total, err := vs.voteRepo.ListUserVotes(ctx, req.UserID, req.Page, req.PageSize, activityTypes) if err != nil { - return + return nil, err } lang := handler.GetLangByCtx(ctx) - resp := make([]*schema.GetVoteWithPageResp, 0) + votes := make([]*schema.GetVoteWithPageResp, 0) for _, voteInfo := range voteList { objInfo, err := vs.objectService.GetInfo(ctx, voteInfo.ObjectID) if err != nil { @@ -202,7 +195,65 @@ func (vs *VoteService) ListUserVotes(ctx context.Context, req schema.GetVoteWith if objInfo.QuestionStatus == entity.QuestionStatusDeleted { item.Title = translator.Tr(lang, constant.DeletedQuestionTitleTrKey) } - resp = append(resp, item) + votes = append(votes, item) } - return pager.NewPageModel(total, resp), err + return pager.NewPageModel(total, votes), err +} + +func (vs *VoteService) createVoteOperationInfo(ctx context.Context, + userID string, voteUp bool, objectInfo *schema.SimpleObjectInfo) *schema.VoteOperationInfo { + // warp vote operation + voteOperationInfo := &schema.VoteOperationInfo{ + ObjectID: objectInfo.ObjectID, + ObjectType: objectInfo.ObjectType, + ObjectCreatorUserID: objectInfo.ObjectCreatorUserID, + OperatingUserID: userID, + VoteUp: voteUp, + VoteDown: !voteUp, + } + voteOperationInfo.Activities = vs.getActivities(ctx, voteOperationInfo) + return voteOperationInfo +} + +func (vs *VoteService) getActivities(ctx context.Context, op *schema.VoteOperationInfo) ( + activities []*schema.VoteActivity) { + activities = make([]*schema.VoteActivity, 0) + + var actions []string + switch op.ObjectType { + case constant.QuestionObjectType: + if op.VoteUp { + actions = []string{activity_type.QuestionVoteUp, activity_type.QuestionVotedUp} + } else { + actions = []string{activity_type.QuestionVoteDown, activity_type.QuestionVotedDown} + } + case constant.AnswerObjectType: + if op.VoteUp { + actions = []string{activity_type.AnswerVoteUp, activity_type.AnswerVotedUp} + } else { + actions = []string{activity_type.AnswerVoteDown, activity_type.AnswerVotedDown} + } + case constant.CommentObjectType: + actions = []string{activity_type.CommentVoteUp} + } + + for _, action := range actions { + t := &schema.VoteActivity{} + cfg, err := vs.configService.GetConfigByKey(ctx, action) + if err != nil { + log.Warnf("get config by key error: %v", err) + continue + } + t.ActivityType, t.Rank = cfg.ID, cfg.GetIntValue() + + if strings.Contains(action, "voted") { + t.ActivityUserID = op.ObjectCreatorUserID + t.TriggerUserID = op.OperatingUserID + } else { + t.ActivityUserID = op.OperatingUserID + t.TriggerUserID = "0" + } + activities = append(activities, t) + } + return activities } diff --git a/pkg/checker/file_type.go b/pkg/checker/file_type.go index a5918a44..37dfdc42 100644 --- a/pkg/checker/file_type.go +++ b/pkg/checker/file_type.go @@ -22,8 +22,5 @@ func IsSupportedImageFile(file io.Reader, ext string) bool { default: return false } - if err != nil { - return false - } - return true + return err == nil } diff --git a/pkg/gravatar/gravatar_test.go b/pkg/gravatar/gravatar_test.go index 927988aa..14ea1ad1 100644 --- a/pkg/gravatar/gravatar_test.go +++ b/pkg/gravatar/gravatar_test.go @@ -1,6 +1,7 @@ package gravatar import ( + "github.com/answerdev/answer/internal/base/constant" "testing" "github.com/stretchr/testify/assert" @@ -23,7 +24,7 @@ func TestGetAvatarURL(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assert.Equal(t, tt.want, GetAvatarURL(tt.args.email)) + assert.Equal(t, tt.want, GetAvatarURL(constant.DefaultGravatarBaseURL, tt.args.email)) }) } } diff --git a/pkg/random/random_username.go b/pkg/random/random_username.go index 3c8cb1de..35715f9a 100644 --- a/pkg/random/random_username.go +++ b/pkg/random/random_username.go @@ -1,8 +1,8 @@ package random import ( + "crypto/rand" "encoding/hex" - "math/rand" ) func UsernameSuffix() string { diff --git a/pkg/uid/sid.go b/pkg/uid/sid.go index 2e9f5117..27d162e5 100644 --- a/pkg/uid/sid.go +++ b/pkg/uid/sid.go @@ -8,8 +8,6 @@ import ( const salt = int64(100) -var ShortIDSwitch = false - // NumToString num to string func NumToShortID(id int64) string { sid := strconv.FormatInt(id, 10) @@ -45,14 +43,11 @@ func ShortIDToNum(code string) int64 { } func EnShortID(id string) string { - if ShortIDSwitch { - num, err := strconv.ParseInt(id, 10, 64) - if err != nil { - return id - } - return NumToShortID(num) + num, err := strconv.ParseInt(id, 10, 64) + if err != nil { + return id } - return id + return NumToShortID(num) } func DeShortID(sid string) string { diff --git a/pkg/uid/sid_test.go b/pkg/uid/sid_test.go index 96501a98..e91cd0dc 100644 --- a/pkg/uid/sid_test.go +++ b/pkg/uid/sid_test.go @@ -31,7 +31,6 @@ func Test_ShortID(t *testing.T) { func Test_EnDeShortID(t *testing.T) { nums := []string{"0", "1", "10", "100", "1000", "10000", "100000", "1234567", "10000000000000000", "10010000000001316", "19930000000001316"} - ShortIDSwitch = true for _, num := range nums { code := EnShortID(num) denum := DeShortID(code) @@ -49,6 +48,6 @@ func Test_Demo(t *testing.T) { // https://answer.dev.segmentfault.com/questions/D112 func Test_DeCode(t *testing.T) { - aaa := DeShortID("D112") + aaa := DeShortID("D1w2") spew.Dump(aaa) } diff --git a/plugin/plugin.go b/plugin/plugin.go index 7f6e4a41..0b1c9de0 100644 --- a/plugin/plugin.go +++ b/plugin/plugin.go @@ -56,6 +56,10 @@ func Register(p Base) { if _, ok := p.(Agent); ok { registerAgent(p.(Agent)) } + + if _, ok := p.(Search); ok { + registerSearch(p.(Search)) + } } type Stack[T Base] struct { diff --git a/plugin/search.go b/plugin/search.go new file mode 100644 index 00000000..4d9d1921 --- /dev/null +++ b/plugin/search.go @@ -0,0 +1,97 @@ +package plugin + +import ( + "context" +) + +type SearchResult struct { + // ID content ID + ID string + // Type content type, example: "answer", "question" + Type string +} + +type SearchContent struct { + ObjectID string `json:"objectID"` + Title string `json:"title"` + Type string `json:"type"` + Content string `json:"content"` + Answers int64 `json:"answers"` + Status SearchContentStatus `json:"status"` + Tags []string `json:"tags"` + QuestionID string `json:"questionID"` + UserID string `json:"userID"` + Views int64 `json:"views"` + Created int64 `json:"created"` + Active int64 `json:"active"` + Score int64 `json:"score"` + HasAccepted bool `json:"hasAccepted"` +} + +type SearchBasicCond struct { + // From zero-based page number + Page int + // Page size + PageSize int + + // The keywords for search. + Words []string + // TagIDs is a list of tag IDs. + TagIDs []string + // The object's owner user ID. + UserID string + // The order of the search result. + Order SearchOrderCond + + // Weathers the question is accepted or not. Only support search question. + QuestionAccepted SearchAcceptedCond + // Weathers the answer is accepted or not. Only support search answer. + AnswerAccepted SearchAcceptedCond + + // Only support search answer. + QuestionID string + + // greater than or equal to the number of votes. + VoteAmount int + // greater than or equal to the number of views. + ViewAmount int + // greater than or equal to the number of answers. Only support search question. + AnswerAmount int +} + +type SearchAcceptedCond int +type SearchContentStatus int +type SearchOrderCond string + +const ( + AcceptedCondAll SearchAcceptedCond = iota + AcceptedCondTrue + AcceptedCondFalse +) + +const ( + SearchContentStatusAvailable = 1 + SearchContentStatusDeleted = 10 +) + +const ( + SearchNewestOrder SearchOrderCond = "newest" + SearchActiveOrder SearchOrderCond = "active" + SearchScoreOrder SearchOrderCond = "score" + SearchRelevanceOrder SearchOrderCond = "relevance" +) + +type Search interface { + Base + SearchContents(ctx context.Context, cond *SearchBasicCond) (res []SearchResult, total int64, err error) + SearchQuestions(ctx context.Context, cond *SearchBasicCond) (res []SearchResult, total int64, err error) + SearchAnswers(ctx context.Context, cond *SearchBasicCond) (res []SearchResult, total int64, err error) + UpdateContent(ctx context.Context, contentID string, content *SearchContent) error + DeleteContent(ctx context.Context, contentID string) error +} + +var ( + // CallUserCenter is a function that calls all registered parsers + CallSearch, + registerSearch = MakePlugin[Search](false) +) diff --git a/ui/package.json b/ui/package.json index f6d869d7..237a00b5 100644 --- a/ui/package.json +++ b/ui/package.json @@ -13,8 +13,8 @@ }, "dependencies": { "axios": "^0.27.2", - "bootstrap": "^5.2.0", - "bootstrap-icons": "1.10.4", + "bootstrap": "^5.3.0", + "bootstrap-icons": "^1.10.5", "classnames": "^2.3.1", "codemirror": "5.65.0", "color": "^4.2.3", @@ -31,14 +31,13 @@ "qrcode": "^1.5.1", "qs": "^6.11.0", "react": "^18.2.0", - "react-bootstrap": "^2.5.0", + "react-bootstrap": "^2.7.4", "react-dom": "^18.2.0", "react-helmet-async": "^1.3.0", "react-i18next": "^11.18.3", "react-router-dom": "^6.8.1", "semver": "^7.3.8", "swr": "^1.3.0", - "urlcat": "^3.0.0", "zustand": "^4.1.1" }, "devDependencies": { diff --git a/ui/pnpm-lock.yaml b/ui/pnpm-lock.yaml index aa286c88..80e9de86 100644 --- a/ui/pnpm-lock.yaml +++ b/ui/pnpm-lock.yaml @@ -20,8 +20,8 @@ specifiers: '@typescript-eslint/eslint-plugin': ^5.0.0 '@typescript-eslint/parser': ^5.33.0 axios: ^0.27.2 - bootstrap: ^5.2.0 - bootstrap-icons: 1.10.4 + bootstrap: ^5.3.0 + bootstrap-icons: ^1.10.5 classnames: ^2.3.1 codemirror: 5.65.0 color: ^4.2.3 @@ -57,7 +57,7 @@ specifiers: qs: ^6.11.0 react: ^18.2.0 react-app-rewired: ^2.2.1 - react-bootstrap: ^2.5.0 + react-bootstrap: ^2.7.4 react-dom: ^18.2.0 react-helmet-async: ^1.3.0 react-i18next: ^11.18.3 @@ -67,14 +67,13 @@ specifiers: semver: ^7.3.8 swr: ^1.3.0 typescript: ^4.8.3 - urlcat: ^3.0.0 yaml-loader: ^0.8.0 zustand: ^4.1.1 dependencies: axios: 0.27.2 - bootstrap: 5.2.1_@popperjs+core@2.11.7 - bootstrap-icons: 1.10.4 + bootstrap: 5.3.0_@popperjs+core@2.11.8 + bootstrap-icons: 1.10.5 classnames: 2.3.2 codemirror: 5.65.0 color: 4.2.3 @@ -91,14 +90,13 @@ dependencies: qrcode: 1.5.1 qs: 6.11.0 react: 18.2.0 - react-bootstrap: 2.5.0_7ey2zzynotv32rpkwno45fsx4e + react-bootstrap: 2.7.4_7ey2zzynotv32rpkwno45fsx4e react-dom: 18.2.0_react@18.2.0 react-helmet-async: 1.3.0_biqbaboplfbrettd7655fr4n2y react-i18next: 11.18.6_ulhmqqxshznzmtuvahdi5nasbq react-router-dom: 6.8.1_biqbaboplfbrettd7655fr4n2y semver: 7.3.8 swr: 1.3.0_react@18.2.0 - urlcat: 3.0.0 zustand: 4.1.1_react@18.2.0 devDependencies: @@ -1528,6 +1526,12 @@ packages: dependencies: regenerator-runtime: 0.13.9 + /@babel/runtime/7.22.5: + resolution: {integrity: sha512-ecjvYlnAaZ/KVneE/OdKYBYfgXV3Ptu6zQWmgEF7vwKhQnvVS6bjMD2XYgj+SNvQ1GfK/pjgokfPkC/2CO8CuA==} + engines: {node: '>=6.9.0'} + dependencies: + regenerator-runtime: 0.13.11 + /@babel/template/7.18.10: resolution: {integrity: sha512-TI+rCtooWHr3QJ27kJxfjutghu44DLnasDMwpDqCXVTal9RLp3RSYNh4NdBrRP2cQAoG9A8juOQl6P6oZG4JxA==} engines: {node: '>=6.9.0'} @@ -1689,7 +1693,7 @@ packages: cosmiconfig-typescript-loader: 4.1.0_2uclxasecupgvdn72amnhmyg7y lodash: 4.17.21 resolve-from: 5.0.0 - ts-node: 10.9.1_5bkdw6noa5sa7givrguqy7ejvm + ts-node: 10.9.1_yxpazyh7n5pql7jdaglasgwqki typescript: 4.9.5 transitivePeerDependencies: - '@swc/core' @@ -2084,7 +2088,7 @@ packages: collect-v8-coverage: 1.0.1 exit: 0.1.2 glob: 7.2.3 - graceful-fs: 4.2.10 + graceful-fs: 4.2.11 istanbul-lib-coverage: 3.2.0 istanbul-lib-instrument: 5.2.0 istanbul-lib-report: 3.0.0 @@ -2113,7 +2117,7 @@ packages: engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} dependencies: callsites: 3.1.0 - graceful-fs: 4.2.10 + graceful-fs: 4.2.11 source-map: 0.6.1 /@jest/test-result/27.5.1: @@ -2139,7 +2143,7 @@ packages: engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} dependencies: '@jest/test-result': 27.5.1 - graceful-fs: 4.2.10 + graceful-fs: 4.2.11 jest-haste-map: 27.5.1 jest-runtime: 27.5.1 transitivePeerDependencies: @@ -2336,20 +2340,16 @@ packages: webpack: 5.74.0 webpack-dev-server: 4.11.1_webpack@5.74.0 - /@popperjs/core/2.11.6: - resolution: {integrity: sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==} + /@popperjs/core/2.11.8: + resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} dev: false - /@popperjs/core/2.11.7: - resolution: {integrity: sha512-Cr4OjIkipTtcXKjAsm8agyleBuDHvxzeBoa1v543lbv1YaIwQjESsVcmjiWiPEbC1FIeHOG/Op9kdCmAmiS3Kw==} - dev: false - - /@react-aria/ssr/3.3.0_react@18.2.0: - resolution: {integrity: sha512-yNqUDuOVZIUGP81R87BJVi/ZUZp/nYOBXbPsRe7oltJOfErQZD+UezMpw4vM2KRz18cURffvmC8tJ6JTeyDtaQ==} + /@react-aria/ssr/3.6.0_react@18.2.0: + resolution: {integrity: sha512-OFiYQdv+Yk7AO7IsQu/fAEPijbeTwrrEYvdNoJ3sblBBedD5j5fBTNWrUPNVlwC4XWWnWTCMaRIVsJujsFiWXg==} peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 dependencies: - '@babel/runtime': 7.19.0 + '@swc/helpers': 0.4.14 react: 18.2.0 dev: false @@ -2358,8 +2358,8 @@ packages: engines: {node: '>=14'} dev: false - /@restart/hooks/0.4.7_react@18.2.0: - resolution: {integrity: sha512-ZbjlEHcG+FQtpDPHd7i4FzNNvJf2enAwZfJbpM8CW7BhmOAbsHpZe3tsHwfQUrBuyrxWqPYp2x5UMnilWcY22A==} + /@restart/hooks/0.4.9_react@18.2.0: + resolution: {integrity: sha512-3BekqcwB6Umeya+16XPooARn4qEPW6vNvwYnlofIYe6h9qG1/VeD7UvShCWx11eFz5ELYmwIEshz+MkPX3wjcQ==} peerDependencies: react: '>=16.8.0' dependencies: @@ -2367,22 +2367,22 @@ packages: react: 18.2.0 dev: false - /@restart/ui/1.4.0_biqbaboplfbrettd7655fr4n2y: - resolution: {integrity: sha512-5dDj5uDzUgK1iijWPRg6AnxjkHM04XhTQDJirM1h/8tIc7KyLtF9YyjcCpNEn259hPMXswpkfXKNgiag0skPFg==} + /@restart/ui/1.6.6_biqbaboplfbrettd7655fr4n2y: + resolution: {integrity: sha512-eC3puKuWE1SRYbojWHXnvCNHGgf3uzHCb6JOhnF4OXPibOIPEkR1sqDSkL643ydigxwh+ruCa1CmYHlzk7ikKA==} peerDependencies: react: '>=16.14.0' react-dom: '>=16.14.0' dependencies: - '@babel/runtime': 7.19.0 - '@popperjs/core': 2.11.6 - '@react-aria/ssr': 3.3.0_react@18.2.0 - '@restart/hooks': 0.4.7_react@18.2.0 + '@babel/runtime': 7.22.5 + '@popperjs/core': 2.11.8 + '@react-aria/ssr': 3.6.0_react@18.2.0 + '@restart/hooks': 0.4.9_react@18.2.0 '@types/warning': 3.0.0 dequal: 2.0.3 dom-helpers: 5.2.1 react: 18.2.0 react-dom: 18.2.0_react@18.2.0 - uncontrollable: 7.2.1_react@18.2.0 + uncontrollable: 8.0.2_react@18.2.0 warning: 4.0.3 dev: false @@ -2555,6 +2555,12 @@ packages: transitivePeerDependencies: - supports-color + /@swc/helpers/0.4.14: + resolution: {integrity: sha512-4C7nX/dvpzB7za4Ql9K81xK3HPxCpHMgwTZVyf+9JQ6VUbn9jjZVN7/Nkdz/Ugzs2CSjqnL/UPXroiVBVHUWUw==} + dependencies: + tslib: 2.4.0 + dev: false + /@testing-library/dom/8.18.1: resolution: {integrity: sha512-oEvsm2B/WtcHKE+IcEeeCqNU/ltFGaVyGbpcm4g/2ytuT49jrlH9x5qRKL/H3A6yfM4YAbSbC0ceT5+9CEXnLg==} engines: {node: '>=12'} @@ -2837,8 +2843,8 @@ packages: '@types/react': 18.0.20 dev: true - /@types/react-transition-group/4.4.5: - resolution: {integrity: sha512-juKD/eiSM3/xZYzjuzH6ZwpP+/lejltmiS3QEzV/vmb/Q8+HfDmxu+Baga8UEMGBqV88Nbg4l2hY/K2DkyaLLA==} + /@types/react-transition-group/4.4.6: + resolution: {integrity: sha512-VnCdSxfcm08KjsJVQcfBmhEQAPnLB8G08hAxn39azX1qYBQ/5RVQuoHuKIcfKOdncuaUvEpFKFzEvbtIMsfVew==} dependencies: '@types/react': 18.0.20 dev: false @@ -3798,16 +3804,16 @@ packages: /boolbase/1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} - /bootstrap-icons/1.10.4: - resolution: {integrity: sha512-eI3HyIUmpGKRiRv15FCZccV+2sreGE2NnmH8mtxV/nPOzQVu0sPEj8HhF1MwjJ31IhjF0rgMvtYOX5VqIzcb/A==} + /bootstrap-icons/1.10.5: + resolution: {integrity: sha512-oSX26F37V7QV7NCE53PPEL45d7EGXmBgHG3pDpZvcRaKVzWMqIRL9wcqJUyEha1esFtM3NJzvmxFXDxjJYD0jQ==} dev: false - /bootstrap/5.2.1_@popperjs+core@2.11.7: - resolution: {integrity: sha512-UQi3v2NpVPEi1n35dmRRzBJFlgvWHYwyem6yHhuT6afYF+sziEt46McRbT//kVXZ7b1YUYEVGdXEH74Nx3xzGA==} + /bootstrap/5.3.0_@popperjs+core@2.11.8: + resolution: {integrity: sha512-UnBV3E3v4STVNQdms6jSGO2CvOkjUMdDAVR2V5N4uCMdaIkaQjbcEAMqRimDHIs4uqBYzDAKCQwCB+97tJgHQw==} peerDependencies: - '@popperjs/core': ^2.11.6 + '@popperjs/core': ^2.11.7 dependencies: - '@popperjs/core': 2.11.7 + '@popperjs/core': 2.11.8 dev: false /brace-expansion/1.1.11: @@ -4252,7 +4258,7 @@ packages: dependencies: '@types/node': 14.18.29 cosmiconfig: 7.0.1 - ts-node: 10.9.1_5bkdw6noa5sa7givrguqy7ejvm + ts-node: 10.9.1_yxpazyh7n5pql7jdaglasgwqki typescript: 4.9.5 dev: true @@ -5223,7 +5229,7 @@ packages: /dom-helpers/5.2.1: resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} dependencies: - '@babel/runtime': 7.19.0 + '@babel/runtime': 7.22.5 csstype: 3.1.1 dev: false @@ -5348,7 +5354,7 @@ packages: resolution: {integrity: sha512-T0yTFjdpldGY8PmuXXR0PyQ1ufZpEGiHVrp7zHKB7jdR4qlmZHhONVM5AQOAWXuF/w3dnHbEQVrNptJgt7F+cQ==} engines: {node: '>=10.13.0'} dependencies: - graceful-fs: 4.2.10 + graceful-fs: 4.2.11 tapable: 2.2.1 /enhanced-resolve/5.13.0: @@ -7038,7 +7044,7 @@ packages: ci-info: 3.4.0 deepmerge: 4.2.2 glob: 7.2.3 - graceful-fs: 4.2.10 + graceful-fs: 4.2.11 jest-circus: 27.5.1 jest-environment-jsdom: 27.5.1 jest-environment-node: 27.5.1 @@ -7210,7 +7216,7 @@ packages: '@jest/types': 27.5.1 '@types/stack-utils': 2.0.1 chalk: 4.1.2 - graceful-fs: 4.2.10 + graceful-fs: 4.2.11 micromatch: 4.0.5 pretty-format: 27.5.1 slash: 3.0.0 @@ -7293,7 +7299,7 @@ packages: '@types/node': 16.11.59 chalk: 4.1.2 emittery: 0.8.1 - graceful-fs: 4.2.10 + graceful-fs: 4.2.11 jest-docblock: 27.5.1 jest-environment-jsdom: 27.5.1 jest-environment-node: 27.5.1 @@ -7328,7 +7334,7 @@ packages: collect-v8-coverage: 1.0.1 execa: 5.1.1 glob: 7.2.3 - graceful-fs: 4.2.10 + graceful-fs: 4.2.11 jest-haste-map: 27.5.1 jest-message-util: 27.5.1 jest-mock: 27.5.1 @@ -7346,7 +7352,7 @@ packages: engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} dependencies: '@types/node': 16.11.59 - graceful-fs: 4.2.10 + graceful-fs: 4.2.11 /jest-snapshot/27.5.1: resolution: {integrity: sha512-yYykXI5a0I31xX67mgeLw1DZ0bJB+gpq5IpSuCAoyDi0+BhgU/RIrL+RTzDmkNTchvDFWKP8lp+w/42Z3us5sA==} @@ -7364,7 +7370,7 @@ packages: babel-preset-current-node-syntax: 1.0.1_@babel+core@7.19.1 chalk: 4.1.2 expect: 27.5.1 - graceful-fs: 4.2.10 + graceful-fs: 4.2.11 jest-diff: 27.5.1 jest-get-type: 27.5.1 jest-haste-map: 27.5.1 @@ -7599,7 +7605,7 @@ packages: dependencies: universalify: 2.0.0 optionalDependencies: - graceful-fs: 4.2.10 + graceful-fs: 4.2.11 /jsonp/0.2.1: resolution: {integrity: sha512-pfog5gdDxPdV4eP7Kg87M8/bHgshlZ5pybl+yKxAnCZ5O7lCIn7Ixydj03wOlnDQesky2BPyA91SQ+5Y/mNwzw==} @@ -9329,8 +9335,8 @@ packages: semver: 5.7.1 dev: true - /react-bootstrap/2.5.0_7ey2zzynotv32rpkwno45fsx4e: - resolution: {integrity: sha512-j/aLR+okzbYk61TM3eDOU1NqOqnUdwyVrF+ojoCRUxPdzc2R0xXvqyRsjSoyRoCo7n82Fs/LWjPCin/QJNdwvA==} + /react-bootstrap/2.7.4_7ey2zzynotv32rpkwno45fsx4e: + resolution: {integrity: sha512-EPKPwhfbxsKsNBhJBitJwqul9fvmlYWSft6jWE2EpqhEyjhqIqNihvQo2onE5XtS+QHOavUSNmA+8Lnv5YeAyg==} peerDependencies: '@types/react': '>=16.14.8' react: '>=16.14.0' @@ -9339,11 +9345,11 @@ packages: '@types/react': optional: true dependencies: - '@babel/runtime': 7.19.0 - '@restart/hooks': 0.4.7_react@18.2.0 - '@restart/ui': 1.4.0_biqbaboplfbrettd7655fr4n2y + '@babel/runtime': 7.22.5 + '@restart/hooks': 0.4.9_react@18.2.0 + '@restart/ui': 1.6.6_biqbaboplfbrettd7655fr4n2y '@types/react': 18.0.20 - '@types/react-transition-group': 4.4.5 + '@types/react-transition-group': 4.4.6 classnames: 2.3.2 dom-helpers: 5.2.1 invariant: 2.2.4 @@ -9590,7 +9596,7 @@ packages: react: '>=16.6.0' react-dom: '>=16.6.0' dependencies: - '@babel/runtime': 7.19.0 + '@babel/runtime': 7.22.5 dom-helpers: 5.2.1 loose-envify: 1.4.0 prop-types: 15.8.1 @@ -9676,13 +9682,16 @@ packages: /regenerate/1.4.2: resolution: {integrity: sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==} + /regenerator-runtime/0.13.11: + resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==} + /regenerator-runtime/0.13.9: resolution: {integrity: sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==} /regenerator-transform/0.15.0: resolution: {integrity: sha512-LsrGtPmbYg19bcPHwdtmXwbW+TqNvtY4riE3P83foeHRroMbH6/2ddFBfab3t7kbzc7v7p4wbkIecHImqt0QNg==} dependencies: - '@babel/runtime': 7.19.0 + '@babel/runtime': 7.22.5 /regex-parser/2.2.11: resolution: {integrity: sha512-jbD/FT0+9MBU2XAZluI7w2OBs1RBi6p9M83nkoZayQXXU9e8Robt69FcZc7wU4eJD/YFTjn1JdCk3rbMJajz8Q==} @@ -10753,6 +10762,37 @@ packages: v8-compile-cache-lib: 3.0.1 yn: 3.1.1 + /ts-node/10.9.1_yxpazyh7n5pql7jdaglasgwqki: + resolution: {integrity: sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==} + hasBin: true + peerDependencies: + '@swc/core': '>=1.2.50' + '@swc/wasm': '>=1.2.50' + '@types/node': '*' + typescript: '>=2.7' + peerDependenciesMeta: + '@swc/core': + optional: true + '@swc/wasm': + optional: true + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.9 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.3 + '@types/node': 14.18.29 + acorn: 8.8.0 + acorn-walk: 8.2.0 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.2 + make-error: 1.3.6 + typescript: 4.9.5 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + dev: true + /tsconfig-paths/3.14.1: resolution: {integrity: sha512-fxDhWnFSLt3VuTwtvJt5fpwxBHg5AdKWMsgcPOOIilyjymcYVZoCQF8fvFRezCNfblEXmi+PcM1eYHeOAgXCOQ==} dependencies: @@ -10849,13 +10889,21 @@ packages: peerDependencies: react: '>=15.0.0' dependencies: - '@babel/runtime': 7.19.0 + '@babel/runtime': 7.22.5 '@types/react': 18.0.20 invariant: 2.2.4 react: 18.2.0 react-lifecycles-compat: 3.0.4 dev: false + /uncontrollable/8.0.2_react@18.2.0: + resolution: {integrity: sha512-/GDx+K1STGtpgTsj5Dj3J51YaKxZDblbCQHTH1zHLuoBEWodj6MjtRVv3TUijj1JYLRLSFsFzN8NV4M3QV4d9w==} + peerDependencies: + react: '>=16.14.0' + dependencies: + react: 18.2.0 + dev: false + /unicode-canonical-property-names-ecmascript/2.0.0: resolution: {integrity: sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==} engines: {node: '>=4'} @@ -10936,12 +10984,6 @@ packages: querystringify: 2.2.0 requires-port: 1.0.0 - /urlcat/3.0.0: - resolution: {integrity: sha512-SSXrIzInzKdWjBfm5iOrPfO6E5Nt0aFs5PTZCauxJTjJE3qhfePAWz8tjGm7dnWMYIAdPGjio51aakunyZHMXQ==} - dependencies: - qs: 6.11.0 - dev: false - /use-sync-external-store/1.2.0_react@18.2.0: resolution: {integrity: sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==} peerDependencies: @@ -11026,7 +11068,7 @@ packages: engines: {node: '>=10.13.0'} dependencies: glob-to-regexp: 0.4.1 - graceful-fs: 4.2.10 + graceful-fs: 4.2.11 /wbuf/1.7.3: resolution: {integrity: sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==} diff --git a/ui/src/common/interface.ts b/ui/src/common/interface.ts index 151a8c39..e5d78bb3 100644 --- a/ui/src/common/interface.ts +++ b/ui/src/common/interface.ts @@ -9,6 +9,11 @@ export interface FormDataType { [prop: string]: FormValue; } +export interface FieldError { + error_field: string; + error_msg: string; +} + export interface Paging { page: number; page_size?: number; @@ -52,7 +57,7 @@ export interface TagInfo extends TagBase { main_tag_slug_name?: string; excerpt?; } -export interface QuestionParams { +export interface QuestionParams extends ImgCodeReq{ title: string; url_title?: string; content: string; @@ -68,7 +73,7 @@ export interface ListResult { list: T[]; } -export interface AnswerParams { +export interface AnswerParams extends ImgCodeReq { content: string; html: string; question_id: string; @@ -169,10 +174,29 @@ export interface PasswordResetReq extends ImgCodeReq { e_mail: string; } -export interface CheckImgReq { - action: 'login' | 'e_mail' | 'find_pass' | 'modify_pass'; +export interface PasswordReplaceReq extends ImgCodeReq { + code: string; + pass: string; } +export interface CaptchaReq extends ImgCodeReq { + verify: ImgCodeRes['verify']; +} + +export type CaptchaKey = + | 'email' + | 'password' + | 'edit_userinfo' + | 'question' + | 'answer' + | 'comment' + | 'edit' + | 'invitation_answer' + | 'search' + | 'report' + | 'delete' + | 'vote'; + export interface SetNoticeReq { notice_switch: boolean; } @@ -222,7 +246,7 @@ export interface AnswerItem { [prop: string]: any; } -export interface PostAnswerReq { +export interface PostAnswerReq extends ImgCodeReq { content: string; html?: string; question_id: string; @@ -425,7 +449,7 @@ export interface FollowParams { /** * @description search request params */ -export interface SearchParams { +export interface SearchParams extends ImgCodeReq { q: string; order: string; page: number; diff --git a/ui/src/components/Actions/index.tsx b/ui/src/components/Actions/index.tsx index 268ff9d1..dfa2102b 100644 --- a/ui/src/components/Actions/index.tsx +++ b/ui/src/components/Actions/index.tsx @@ -6,9 +6,10 @@ import classNames from 'classnames'; import { Icon } from '@/components'; import { loggedUserInfoStore } from '@/stores'; -import { useToast } from '@/hooks'; +import { useToast, useCaptchaModal } from '@/hooks'; import { tryNormalLogged } from '@/utils/guard'; import { bookmark, postVote } from '@/services'; +import * as Types from '@/common/interface'; interface Props { className?: string; @@ -36,6 +37,8 @@ const Index: FC = ({ className, data, source }) => { const { username = '' } = loggedUserInfoStore((state) => state.user); const toast = useToast(); const { t } = useTranslation(); + const vCaptcha = useCaptchaModal('vote'); + useEffect(() => { if (data) { setVotes(data.votesCount); @@ -61,27 +64,39 @@ const Index: FC = ({ className, data, source }) => { return; } const isCancel = (type === 'up' && like) || (type === 'down' && hate); - postVote( - { - object_id: data?.id, - is_cancel: isCancel, - }, - type, - ) - .then((res) => { - setVotes(res.votes); - setLike(res.vote_status === 'vote_up'); - setHated(res.vote_status === 'vote_down'); - }) - .catch((err) => { - const errMsg = err?.value; - if (errMsg) { - toast.onShow({ - msg: errMsg, - variant: 'danger', - }); - } - }); + vCaptcha.check(() => { + const imgCode: Types.ImgCodeReq = { + captcha_id: undefined, + captcha_code: undefined, + }; + vCaptcha.resolveCaptchaReq(imgCode); + postVote( + { + object_id: data?.id, + is_cancel: isCancel, + ...imgCode, + }, + type, + ) + .then(async (res) => { + await vCaptcha.close(); + setVotes(res.votes); + setLike(res.vote_status === 'vote_up'); + setHated(res.vote_status === 'vote_down'); + }) + .catch((err) => { + if (err?.isError) { + vCaptcha.handleCaptchaError(err.list); + } + const errMsg = err?.value; + if (errMsg) { + toast.onShow({ + msg: errMsg, + variant: 'danger', + }); + } + }); + }); }; const handleBookmark = () => { @@ -131,6 +146,7 @@ const Index: FC = ({ className, data, source }) => { {!data?.hideCollect && ( - - - {captcha?.errorMsg} - - - - -
- -
- - - - ); -}; -export default Index; diff --git a/ui/src/components/Modal/index.tsx b/ui/src/components/Modal/index.tsx index 81764934..226f2fbe 100644 --- a/ui/src/components/Modal/index.tsx +++ b/ui/src/components/Modal/index.tsx @@ -1,6 +1,5 @@ import DefaultModal from './Modal'; import confirm, { Config } from './Confirm'; -import PicAuthCodeModal from './PicAuthCodeModal'; import LoginToContinueModal from './LoginToContinueModal'; type ModalType = typeof DefaultModal & { @@ -14,5 +13,4 @@ Modal.confirm = function (props: Config) { export default Modal; -export { PicAuthCodeModal }; export { LoginToContinueModal }; diff --git a/ui/src/components/Operate/index.tsx b/ui/src/components/Operate/index.tsx index 6bcf2c2f..d322b9d1 100644 --- a/ui/src/components/Operate/index.tsx +++ b/ui/src/components/Operate/index.tsx @@ -4,7 +4,7 @@ import { Link, useNavigate } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { Modal } from '@/components'; -import { useReportModal, useToast } from '@/hooks'; +import { useReportModal, useToast, useCaptchaModal } from '@/hooks'; import { QuestionOperationReq } from '@/common/interface'; import Share from '../Share'; import { @@ -44,6 +44,7 @@ const Index: FC = ({ const toast = useToast(); const navigate = useNavigate(); const reportModal = useReportModal(); + const dCaptcha = useCaptchaModal('delete'); const refreshQuestion = () => { callback?.('default'); @@ -77,14 +78,28 @@ const Index: FC = ({ confirmBtnVariant: 'danger', confirmText: t('delete', { keyPrefix: 'btns' }), onConfirm: () => { - deleteQuestion({ - id: qid, - }).then(() => { - toast.onShow({ - msg: t('post_deleted', { keyPrefix: 'messages' }), - variant: 'success', - }); - callback?.('delete_question'); + dCaptcha.check(() => { + const req = { + id: qid, + captcha_code: undefined, + captcha_id: undefined, + }; + dCaptcha.resolveCaptchaReq(req); + + deleteQuestion(req) + .then(async () => { + await dCaptcha.close(); + toast.onShow({ + msg: t('post_deleted', { keyPrefix: 'messages' }), + variant: 'success', + }); + callback?.('delete_question'); + }) + .catch((ex) => { + if (ex.isError) { + dCaptcha.handleCaptchaError(ex.list); + } + }); }); }, }); @@ -98,15 +113,29 @@ const Index: FC = ({ confirmBtnVariant: 'danger', confirmText: t('delete', { keyPrefix: 'btns' }), onConfirm: () => { - deleteAnswer({ - id: aid, - }).then(() => { - // refresh page - toast.onShow({ - msg: t('tip_answer_deleted'), - variant: 'success', - }); - callback?.('all'); + dCaptcha.check(() => { + const req = { + id: aid, + captcha_code: undefined, + captcha_id: undefined, + }; + dCaptcha.resolveCaptchaReq(req); + + deleteAnswer(req) + .then(async () => { + await dCaptcha.close(); + // refresh page + toast.onShow({ + msg: t('tip_answer_deleted'), + variant: 'success', + }); + callback?.('all'); + }) + .catch((ex) => { + if (ex.isError) { + dCaptcha.handleCaptchaError(ex.list); + } + }); }); }, }); @@ -271,7 +300,7 @@ const Index: FC = ({ ); })} {secondAction.length > 0 && ( - + = ({ path={handleParams(1)} /> - + )} {currentPage >= 5 && ( @@ -182,7 +182,7 @@ const Index: FC = ({ ); })} {totalPage > 5 && realPage + 2 < totalPage && ( - + )} {totalPage > 0 && currentPage < totalPage && ( diff --git a/ui/src/components/SchemaForm/components/Upload.tsx b/ui/src/components/SchemaForm/components/Upload.tsx index df8dbc60..42715627 100644 --- a/ui/src/components/SchemaForm/components/Upload.tsx +++ b/ui/src/components/SchemaForm/components/Upload.tsx @@ -1,6 +1,8 @@ import React, { FC } from 'react'; import { Form } from 'react-bootstrap'; +import classNames from 'classnames'; + import type * as Type from '@/common/interface'; import BrandUpload from '@/components/BrandUpload'; @@ -11,6 +13,7 @@ interface Props { onChange?: (fd: Type.FormDataType) => void; formData: Type.FormDataType; readOnly?: boolean; + imgClassNames?: classNames.Argument; } const Index: FC = ({ type = 'avatar', @@ -19,6 +22,7 @@ const Index: FC = ({ onChange, formData, readOnly = false, + imgClassNames = '', }) => { const fieldObject = formData[fieldName]; const handleChange = (name: string, value: string) => { @@ -41,6 +45,7 @@ const Index: FC = ({ value={fieldObject?.value} readOnly={readOnly} onChange={(value) => handleChange(fieldName, value)} + imgClassNames={imgClassNames} /> = ( uiSchema?.[key] || {}; formData ||= {}; const fieldState = formData[key]; + const uiSimplify = widget === 'legend' || uiOpt?.simplify; let groupClassName: BaseUIOptions['fieldClassName'] = uiOpt?.simplify ? 'mb-2' @@ -249,7 +250,9 @@ const SchemaForm: ForwardRefRenderFunction = ( if (uiOpt?.fieldClassName) { groupClassName = uiOpt.fieldClassName; } + const readOnly = uiOpt?.readOnly || false; + return ( = ( onChange={onChange} formData={formData} readOnly={readOnly} + imgClassNames={ + uiOpt && 'className' in uiOpt ? uiOpt.className : '' + } /> ) : null} {widget === 'textarea' ? ( diff --git a/ui/src/components/SchemaForm/types.ts b/ui/src/components/SchemaForm/types.ts index d7965ca7..f22e6b89 100644 --- a/ui/src/components/SchemaForm/types.ts +++ b/ui/src/components/SchemaForm/types.ts @@ -39,7 +39,7 @@ export interface BaseUIOptions { empty?: string; // Will be appended to the className of the form component itself className?: classnames.Argument; - // The className that will be attached to a form field container + // The className that will be attached to a **form field container** fieldClassName?: classnames.Argument; // Make a form component render into simplified mode readOnly?: boolean; diff --git a/ui/src/components/Share/index.tsx b/ui/src/components/Share/index.tsx index 26c51080..dcab12fe 100644 --- a/ui/src/components/Share/index.tsx +++ b/ui/src/components/Share/index.tsx @@ -71,7 +71,7 @@ const Index: FC = ({ type, qid, aid, title, slugTitle = '' }) => { setShow(true)} style={{ lineHeight: '23px' }}> {t('share.name')} diff --git a/ui/src/components/SideNav/index.scss b/ui/src/components/SideNav/index.scss index a147e390..5a088bf2 100644 --- a/ui/src/components/SideNav/index.scss +++ b/ui/src/components/SideNav/index.scss @@ -5,7 +5,7 @@ width: auto; top: 62px; box-sizing: border-box; - max-height: calc(100vh - 74px - 62px - 24px); + max-height: calc(100vh - 62px); overflow-y: auto; margin-bottom: 8px; } @@ -36,6 +36,9 @@ @media screen and (max-width: 991.9px) { #sideNav { + .nav-wrap { + max-height: fit-content; + } .nav { max-width: 100%; } diff --git a/ui/src/components/TagSelector/index.tsx b/ui/src/components/TagSelector/index.tsx index 51a712df..bbba4f22 100644 --- a/ui/src/components/TagSelector/index.tsx +++ b/ui/src/components/TagSelector/index.tsx @@ -246,8 +246,9 @@ const TagSelector: FC = ({ e.preventDefault(); }}> diff --git a/ui/src/components/Unactivate/index.tsx b/ui/src/components/Unactivate/index.tsx index e45eea5d..f5673d7d 100644 --- a/ui/src/components/Unactivate/index.tsx +++ b/ui/src/components/Unactivate/index.tsx @@ -1,24 +1,21 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState } from 'react'; import { Button, Col } from 'react-bootstrap'; import { Trans, useTranslation } from 'react-i18next'; import { Link } from 'react-router-dom'; -import { PicAuthCodeModal } from '@/components/Modal'; -import type { ImgCodeRes, ImgCodeReq, FormDataType } from '@/common/interface'; +import type { ImgCodeReq, FormDataType } from '@/common/interface'; import { loggedUserInfoStore } from '@/stores'; -import { resendEmail, checkImgCode } from '@/services'; -import { CAPTCHA_CODE_STORAGE_KEY } from '@/common/constants'; -import Storage from '@/utils/storage'; +import { resendEmail } from '@/services'; import { handleFormError } from '@/utils'; +import { useCaptchaModal } from '@/hooks'; interface IProps { - visible: boolean; + visible?: boolean; } -const Index: React.FC = ({ visible = false }) => { +const Index: React.FC = () => { const { t } = useTranslation('translation', { keyPrefix: 'inactive' }); const [isSuccess, setSuccess] = useState(false); - const [showModal, setModalState] = useState(false); const { e_mail } = loggedUserInfoStore((state) => state.user); const [formData, setFormData] = useState({ captcha_code: { @@ -27,75 +24,39 @@ const Index: React.FC = ({ visible = false }) => { errorMsg: '', }, }); - const [imgCode, setImgCode] = useState({ - captcha_id: '', - captcha_img: '', - verify: false, - }); - const getImgCode = () => { - checkImgCode({ - action: 'e_mail', - }).then((res) => { - setImgCode(res); - }); - }; + const emailCaptcha = useCaptchaModal('email'); - const submit = (e?: any) => { - if (e) { - e.preventDefault(); - } - let obj: ImgCodeReq = {}; + const submit = () => { + let req: ImgCodeReq = {}; + const imgCode = emailCaptcha.getCaptcha(); if (imgCode.verify) { - const code = Storage.get(CAPTCHA_CODE_STORAGE_KEY) || ''; - obj = { - captcha_code: code, + req = { + captcha_code: imgCode.captcha_code, captcha_id: imgCode.captcha_id, }; } - resendEmail(obj) + resendEmail(req) .then(() => { + emailCaptcha.close(); setSuccess(true); - setModalState(false); }) .catch((err) => { if (err.isError) { + emailCaptcha.handleCaptchaError(err.list); const data = handleFormError(err, formData); setFormData({ ...data }); } - }) - .finally(() => { - getImgCode(); }); }; - const onSentEmail = () => { - if (imgCode.verify) { - setModalState(true); - if (!formData.captcha_code.value) { - setFormData({ - captcha_code: { - value: '', - isInvalid: false, - errorMsg: t('msg.empty'), - }, - }); - } - return; - } - submit(); + const onSentEmail = (evt) => { + evt.preventDefault(); + emailCaptcha.check(() => { + submit(); + }); }; - const handleChange = (params: FormDataType) => { - setFormData({ ...formData, ...params }); - }; - - useEffect(() => { - if (visible) { - getImgCode(); - } - }, [visible]); - return ( {isSuccess ? ( @@ -124,18 +85,6 @@ const Index: React.FC = ({ visible = false }) => { )} - - setModalState(false)} - /> ); }; diff --git a/ui/src/components/UserCard/index.tsx b/ui/src/components/UserCard/index.tsx index b012de83..b27abe33 100644 --- a/ui/src/components/UserCard/index.tsx +++ b/ui/src/components/UserCard/index.tsx @@ -32,6 +32,7 @@ const Index: FC = ({ size="40px" className="me-2 d-none d-md-block" searchStr="s=96" + alt={data?.display_name} /> = ({ size="24px" className="me-2 d-block d-md-none" searchStr="s=48" + alt={data?.display_name} /> ) : ( @@ -48,6 +50,7 @@ const Index: FC = ({ size="40px" className="me-2 d-none d-md-block" searchStr="s=96" + alt={data?.display_name} /> = ({ size="24px" className="me-2 d-block d-md-none" searchStr="s=48" + alt={data?.display_name} /> )} diff --git a/ui/src/components/index.ts b/ui/src/components/index.ts index 8639d4d6..c8675e03 100644 --- a/ui/src/components/index.ts +++ b/ui/src/components/index.ts @@ -14,7 +14,6 @@ import Operate from './Operate'; import UserCard from './UserCard'; import Pagination from './Pagination'; import Comment from './Comment'; -import PicAuthCodeModal from './Modal/PicAuthCodeModal'; import TextArea from './TextArea'; import Mentions from './Mentions'; import FormatTime from './FormatTime'; @@ -26,7 +25,6 @@ import FollowingTags from './FollowingTags'; import QueryGroup from './QueryGroup'; import BrandUpload from './BrandUpload'; import SchemaForm, { JSONSchema, UISchema, initFormData } from './SchemaForm'; -import Labels from './LabelsCard'; import DiffContent from './DiffContent'; import Customize from './Customize'; import CustomizeTheme from './CustomizeTheme'; @@ -60,7 +58,6 @@ export { UserCard, Pagination, Comment, - PicAuthCodeModal, TextArea, Mentions, FormatTime, @@ -74,7 +71,6 @@ export { BrandUpload, SchemaForm, initFormData, - Labels, DiffContent, Customize, CustomizeTheme, diff --git a/ui/src/hooks/index.ts b/ui/src/hooks/index.ts index 756d8724..0aa11a3b 100644 --- a/ui/src/hooks/index.ts +++ b/ui/src/hooks/index.ts @@ -10,6 +10,8 @@ import useChangePasswordModal from './useChangePasswordModal'; import usePageTags from './usePageTags'; import useLoginRedirect from './useLoginRedirect'; import usePromptWithUnload from './usePrompt'; +import useActivationEmailModal from './useActivationEmailModal'; +import useCaptchaModal from './useCaptchaModal'; export { useTagModal, @@ -24,4 +26,6 @@ export { usePageTags, useLoginRedirect, usePromptWithUnload, + useActivationEmailModal, + useCaptchaModal, }; diff --git a/ui/src/hooks/useActivationEmailModal/index.tsx b/ui/src/hooks/useActivationEmailModal/index.tsx new file mode 100644 index 00000000..c0958559 --- /dev/null +++ b/ui/src/hooks/useActivationEmailModal/index.tsx @@ -0,0 +1,149 @@ +import { useLayoutEffect, useState, useRef } from 'react'; +import { Modal, Button } from 'react-bootstrap'; +import { useTranslation } from 'react-i18next'; + +import ReactDOM from 'react-dom/client'; + +import type * as Type from '@/common/interface'; +import { SchemaForm, JSONSchema, UISchema, initFormData } from '@/components'; +import { handleFormError } from '@/utils'; +import { getUserActivation, postUserActivation } from '@/services'; +import { useToast } from '@/hooks'; + +const div = document.createElement('div'); +const root = ReactDOM.createRoot(div); + +interface IProps { + title?: string; + onConfirm?: (formData: any) => Promise; +} +const useChangePasswordModal = (props: IProps = {}) => { + const { t } = useTranslation('translation', { + keyPrefix: 'inactive', + }); + + const { title = t('btn_name') } = props; + const [visible, setVisibleState] = useState(false); + const userId = useRef(''); + const isLoading = useRef(false); + const Toast = useToast(); + + const schema: JSONSchema = { + title: t('btn_name'), + properties: { + activationUrl: { + type: 'string', + title: t('resend_email.url_label'), + description: t('resend_email.url_text'), + }, + }, + }; + const uiSchema: UISchema = { + activationUrl: { + 'ui:options': { + readOnly: true, + }, + }, + }; + const [formData, setFormData] = useState( + initFormData(schema), + ); + + const formRef = useRef<{ + validator: () => Promise; + }>(null); + + const getActivationUrl = () => { + return getUserActivation(userId.current).then((resp) => { + if (resp?.activation_url) { + setFormData({ + ...formData, + activationUrl: { + value: resp.activation_url, + isInvalid: false, + errorMsg: '', + }, + }); + } + }); + }; + + const onClose = () => { + setVisibleState(false); + userId.current = ''; + setFormData(initFormData(schema)); + }; + + const onShow = async (user_id: string) => { + if (!user_id) { + return; + } + userId.current = user_id; + await getActivationUrl(); + setVisibleState(true); + }; + + const handleSubmit = async (event) => { + event.preventDefault(); + event.stopPropagation(); + isLoading.current = true; + postUserActivation(userId.current) + .then(() => { + Toast.onShow({ + msg: t('sent_success', { keyPrefix: 'toast' }), + variant: 'success', + }); + onClose(); + }) + .catch((err) => { + if (err.isError) { + const data = handleFormError(err, formData); + setFormData({ ...data }); + } + }) + .finally(() => { + isLoading.current = false; + }); + }; + + const handleOnChange = (data) => { + setFormData(data); + }; + + useLayoutEffect(() => { + root.render( + + + {title} + + + + + + + + + , + ); + }); + return { + onClose, + onShow, + }; +}; + +export default useChangePasswordModal; diff --git a/ui/src/hooks/useCaptchaModal/index.tsx b/ui/src/hooks/useCaptchaModal/index.tsx new file mode 100644 index 00000000..50b38587 --- /dev/null +++ b/ui/src/hooks/useCaptchaModal/index.tsx @@ -0,0 +1,264 @@ +import { useEffect, useRef, useState, useLayoutEffect } from 'react'; +import { Modal, Form, Button, InputGroup } from 'react-bootstrap'; +import { useTranslation } from 'react-i18next'; + +import ReactDOM from 'react-dom/client'; + +import { Icon } from '@/components'; +import type { + FormValue, + ImgCodeRes, + CaptchaKey, + FieldError, + ImgCodeReq, +} from '@/common/interface'; +import { checkImgCode } from '@/services'; + +type SubmitCallback = { + (): void; +}; + +const Index = (captchaKey: CaptchaKey) => { + const refRoot = useRef(null); + if (refRoot.current === null) { + // @ts-ignore + refRoot.current = ReactDOM.createRoot(document.createElement('div')); + } + + const { t } = useTranslation('translation', { keyPrefix: 'pic_auth_code' }); + const refKey = useRef(captchaKey); + const refCallback = useRef(); + const pending = useRef(false); + const autoInitCaptchaData = /email/i.test(refKey.current); + + const [stateShow, setStateShow] = useState(false); + const [captcha, setCaptcha] = useState({ + captcha_id: '', + captcha_img: '', + verify: false, + }); + const [imgCode, setImgCode] = useState({ + value: '', + isInvalid: false, + errorMsg: '', + }); + const refCaptcha = useRef(captcha); + const refImgCode = useRef(imgCode); + + const fetchCaptchaData = () => { + pending.current = true; + checkImgCode(refKey.current) + .then((resp) => { + setCaptcha(resp); + }) + .finally(() => { + pending.current = false; + }); + }; + + const resetCapture = () => { + setCaptcha({ + captcha_id: '', + captcha_img: '', + verify: false, + }); + }; + + const resetImgCode = () => { + setImgCode({ + value: '', + isInvalid: false, + errorMsg: '', + }); + }; + const resetCallback = () => { + refCallback.current = undefined; + }; + + const show = () => { + if (!stateShow) { + setStateShow(true); + } + }; + /** + * There are some cases where the React scheduler cancels the execution of some functions, + * which prevents them from closing properly: + * for example, if the parent component uninstalls the child component directly, + * and the `captchaModal.close()` call is inside the child component. + * In this case, call `await captchaModal.close()` and wait for the close action to complete. + */ + const close = () => { + setStateShow(false); + resetCapture(); + resetImgCode(); + resetCallback(); + + const p = new Promise((resolve) => { + setTimeout(resolve, 50); + }); + return p; + }; + + const handleCaptchaError = (fel: FieldError[] = []) => { + const captchaErr = fel.find((o) => { + return o.error_field === 'captcha_code'; + }); + + const ri = refImgCode.current; + if (captchaErr) { + /** + * `imgCode.value` No value but a validation error is received, + * indicating that it is the first time the interface has returned a CAPTCHA error, + * triggering the CAPTCHA logic. There is no need to display the error message at this point. + */ + if (ri.value) { + setImgCode({ + ...ri, + isInvalid: true, + errorMsg: captchaErr.error_msg, + }); + } + fetchCaptchaData(); + show(); + } else { + close(); + } + // Assist business logic in filtering CAPTCHA error messages when necessary + return captchaErr; + }; + + const handleChange = (evt) => { + evt.preventDefault(); + setImgCode({ + value: evt.target.value || '', + isInvalid: false, + errorMsg: '', + }); + }; + + const getCaptcha = () => { + const rc = refCaptcha.current; + const ri = refImgCode.current; + const r = { + verify: !!rc?.verify, + captcha_id: rc?.captcha_id, + captcha_code: ri.value, + }; + + return r; + }; + + const resolveCaptchaReq = (req: ImgCodeReq) => { + const r = getCaptcha(); + if (r.verify) { + req.captcha_code = r.captcha_code; + req.captcha_id = r.captcha_id; + } + }; + + const handleSubmit = (evt) => { + evt.preventDefault(); + if (!imgCode.value) { + return; + } + + if (refCallback.current) { + refCallback.current(); + } + }; + + useEffect(() => { + if (autoInitCaptchaData) { + fetchCaptchaData(); + } + }, []); + + useLayoutEffect(() => { + refImgCode.current = imgCode; + refCaptcha.current = captcha; + }, [captcha, imgCode]); + + useEffect(() => { + // @ts-ignore + refRoot.current.render( + close()} + centered> + + {t('title')} + + +
+ +
+ captcha img +
+ + + + + + {imgCode?.errorMsg} + + +
+ +
+ +
+
+
+
, + ); + }); + + const r = { + close, + show, + check: (submitFunc: SubmitCallback) => { + if (pending.current) { + return false; + } + refCallback.current = submitFunc; + if (captcha?.verify) { + show(); + return false; + } + return submitFunc(); + }, + getCaptcha, + resolveCaptchaReq, + fetchCaptchaData, + handleCaptchaError, + }; + + return r; +}; + +export default Index; diff --git a/ui/src/hooks/useChangePasswordModal/index.tsx b/ui/src/hooks/useChangePasswordModal/index.tsx index f04ac535..f80e3c44 100644 --- a/ui/src/hooks/useChangePasswordModal/index.tsx +++ b/ui/src/hooks/useChangePasswordModal/index.tsx @@ -17,7 +17,7 @@ interface IProps { } const useChangePasswordModal = (props: IProps = {}) => { const { t } = useTranslation('translation', { - keyPrefix: 'admin.users.new_password_modal', + keyPrefix: 'admin.new_password_modal', }); const { title = t('title'), onConfirm } = props; diff --git a/ui/src/hooks/useReportModal/index.tsx b/ui/src/hooks/useReportModal/index.tsx index 748564d9..d0d29e91 100644 --- a/ui/src/hooks/useReportModal/index.tsx +++ b/ui/src/hooks/useReportModal/index.tsx @@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next'; import ReactDOM from 'react-dom/client'; -import { useToast } from '@/hooks'; +import { useToast, useCaptchaModal } from '@/hooks'; import type * as Type from '@/common/interface'; import { reportList, postReport, closeQuestion, putReport } from '@/services'; @@ -37,6 +37,8 @@ const useReportModal = (callback?: () => void) => { const [show, setShow] = useState(false); const [list, setList] = useState([]); + const rCaptcha = useCaptchaModal('report'); + useEffect(() => { const div = document.createElement('div'); rootRef.current.root = ReactDOM.createRoot(div); @@ -103,18 +105,32 @@ const useReportModal = (callback?: () => void) => { return; } if (!params.isBackend && params.action === 'flag') { - postReport({ - source: params.type, - report_type: reportType.type, - object_id: params.id, - content: content.value, - }).then(() => { - toast.onShow({ - msg: t('flag_success', { keyPrefix: 'toast' }), - variant: 'warning', - }); - onClose(); - asyncCallback(); + rCaptcha.check(() => { + const flagReq = { + source: params.type, + report_type: reportType.type, + object_id: params.id, + content: content.value, + captcha_code: undefined, + captcha_id: undefined, + }; + rCaptcha.resolveCaptchaReq(flagReq); + + postReport(flagReq) + .then(async () => { + await rCaptcha.close(); + toast.onShow({ + msg: t('flag_success', { keyPrefix: 'toast' }), + variant: 'warning', + }); + onClose(); + asyncCallback(); + }) + .catch((ex) => { + if (ex.isError) { + rCaptcha.handleCaptchaError(ex.list); + } + }); }); } diff --git a/ui/src/hooks/useTagModal/index.tsx b/ui/src/hooks/useTagModal/index.tsx index 648832d9..69a3e596 100644 --- a/ui/src/hooks/useTagModal/index.tsx +++ b/ui/src/hooks/useTagModal/index.tsx @@ -201,6 +201,7 @@ const useTagModal = (props: IProps = {}) => { {t('form.fields.display_name.label')} { {t('form.fields.slug_name.label')} { const { t } = useTranslation('translation', { - keyPrefix: 'admin.users.user_modal', + keyPrefix: 'admin.user_modal', }); const { title = t('title'), onConfirm } = props; diff --git a/ui/src/index.scss b/ui/src/index.scss index 0f23a1da..7e7df5ee 100644 --- a/ui/src/index.scss +++ b/ui/src/index.scss @@ -26,6 +26,11 @@ body { a { text-decoration: none; } +// If the image does not have a `src` attribute, it must break and may not trigger the `onerror` event. +// With or without the `alt` attribute, it is visually hidden directly. +img[src=""] { + visibility: hidden !important; +} .btn-link { text-decoration: none; @@ -177,9 +182,6 @@ a { display: inline-block; } -.object-fit-contain { - object-fit: contain; -} .fmt { width: 100%; h1 { @@ -249,14 +251,6 @@ a { flex-grow: 1; height: 128px; } -.badge-label { - display: inline-flex; - align-items: center; - justify-content: center; - font-size: 14px; - padding: 1px 0.5rem 2px; - height: 24px; -} .review-text-delete { color: #842029; diff --git a/ui/src/index.tsx b/ui/src/index.tsx index ba0aed28..cc6b8c93 100644 --- a/ui/src/index.tsx +++ b/ui/src/index.tsx @@ -10,6 +10,40 @@ const root = ReactDOM.createRoot( document.getElementById('root') as HTMLElement, ); +/** + * Uniformly hide broken images + * - The `onload` event for elements such as `img` can only be `capture` on `document` (window cannot). + * - For images with an empty `src` attribute, sometimes the browser will simply display the broken image without reporting an 'error' event. + */ +const handleImgLoad = (evt: Event | UIEvent) => { + const { target } = evt; + + if (target === null || !(target instanceof Element)) { + return; + } + if (!/IMG/i.test(target.nodeName)) { + return; + } + + if (/error/i.test(evt.type)) { + target.classList.add('broken'); + const attrSrc = target.getAttribute('src'); + const attrAlt = target.getAttribute('alt')?.trim(); + // Images without the `src` attribute are hidden directly by `css`. + // Images with `alt` content are not hidden - the display of the `alt` content is also hidden. + if (attrSrc && !attrAlt) { + target.classList.add('invisible'); + } + } + + if (/load/i.test(evt.type)) { + target.classList.remove('broken', 'invisible'); + } +}; + +document.addEventListener('error', handleImgLoad, true); +document.addEventListener('load', handleImgLoad, true); + root.render( diff --git a/ui/src/pages/Admin/Answers/index.tsx b/ui/src/pages/Admin/Answers/index.tsx index 1a60332d..e79a1c31 100644 --- a/ui/src/pages/Admin/Answers/index.tsx +++ b/ui/src/pages/Admin/Answers/index.tsx @@ -103,7 +103,7 @@ const Answers: FC = () => { value={curQuery} onChange={handleFilter} size="sm" - type="input" + type="search" placeholder={t('filter.placeholder')} style={{ width: '12.25rem' }} /> diff --git a/ui/src/pages/Admin/Branding/index.tsx b/ui/src/pages/Admin/Branding/index.tsx index 93a86d2d..fd69d226 100644 --- a/ui/src/pages/Admin/Branding/index.tsx +++ b/ui/src/pages/Admin/Branding/index.tsx @@ -76,18 +76,21 @@ const Index: FC = () => { 'ui:widget': 'upload', 'ui:options': { imageType: uploadType, + className: 'object-fit-contain', }, }, mobile_logo: { 'ui:widget': 'upload', 'ui:options': { imageType: uploadType, + className: 'object-fit-contain', }, }, square_icon: { 'ui:widget': 'upload', 'ui:options': { imageType: uploadType, + className: 'object-fit-contain', }, }, favicon: { @@ -95,6 +98,7 @@ const Index: FC = () => { 'ui:options': { acceptType: ',image/x-icon,image/vnd.microsoft.icon', imageType: uploadType, + className: 'object-fit-contain', }, }, }; diff --git a/ui/src/pages/Admin/Dashboard/components/HealthStatus/index.tsx b/ui/src/pages/Admin/Dashboard/components/HealthStatus/index.tsx index 4aca368a..2284d6e5 100644 --- a/ui/src/pages/Admin/Dashboard/components/HealthStatus/index.tsx +++ b/ui/src/pages/Admin/Dashboard/components/HealthStatus/index.tsx @@ -58,7 +58,7 @@ const HealthStatus: FC = ({ data }) => { {t('https')} - {data.https ? t('yes') : t('yes')} + {data.https ? t('yes') : t('no')} {t('uploading_files')} diff --git a/ui/src/pages/Admin/Dashboard/components/SystemInfo/index.tsx b/ui/src/pages/Admin/Dashboard/components/SystemInfo/index.tsx index cbc065c7..99eb7db6 100644 --- a/ui/src/pages/Admin/Dashboard/components/SystemInfo/index.tsx +++ b/ui/src/pages/Admin/Dashboard/components/SystemInfo/index.tsx @@ -20,10 +20,12 @@ const SystemInfo: FC = ({ data }) => { {t('storage_used')} {data.occupying_storage_space} - - {t('uptime')} - {formatUptime(data.app_start_time)} - + {data.app_start_time ? ( + + {t('uptime')} + {formatUptime(data.app_start_time)} + + ) : null} diff --git a/ui/src/pages/Admin/Plugins/Installed/index.tsx b/ui/src/pages/Admin/Plugins/Installed/index.tsx index f0a03031..cde0cb1b 100644 --- a/ui/src/pages/Admin/Plugins/Installed/index.tsx +++ b/ui/src/pages/Admin/Plugins/Installed/index.tsx @@ -112,7 +112,7 @@ const Users: FC = () => { - + {plugin.enabled ? ( diff --git a/ui/src/pages/Admin/Questions/index.tsx b/ui/src/pages/Admin/Questions/index.tsx index 4992d7fa..7b3367eb 100644 --- a/ui/src/pages/Admin/Questions/index.tsx +++ b/ui/src/pages/Admin/Questions/index.tsx @@ -112,7 +112,7 @@ const Questions: FC = () => { { }, }); + const activationEmailModal = useActivationEmailModal(); + const handleAction = (type, user) => { const { user_id, status, role_id, username } = user; if (username === currentUser.username) { @@ -128,6 +131,7 @@ const Users: FC = () => { }); return; } + if (type === 'status') { changeModal.onShow({ id: user_id, @@ -141,9 +145,14 @@ const Users: FC = () => { role_id, }); } + if (type === 'password') { changePasswordModal.onShow(user_id); } + + if (type === 'activation') { + activationEmailModal.onShow(user_id); + } }; const handleFilter = (e) => { @@ -160,16 +169,21 @@ const Users: FC = () => { }, [ucAgent]); const showAddUser = !ucAgent?.enabled || (ucAgent?.enabled && adminUcAgent?.allow_create_user); + const showActionPassword = !ucAgent?.enabled || (ucAgent?.enabled && adminUcAgent?.allow_update_user_password); + const showActionRole = !ucAgent?.enabled || (ucAgent?.enabled && adminUcAgent?.allow_update_user_role); + const showActionStatus = !ucAgent?.enabled || (ucAgent?.enabled && adminUcAgent?.allow_update_user_status); + const showAction = showActionPassword || showActionRole || showActionStatus; + return ( <>

{t('title')}

@@ -193,6 +207,7 @@ const Users: FC = () => { { {data?.list.map((user) => { + const showActionActivation = user.status === 'inactive'; + return ( @@ -266,11 +283,12 @@ const Users: FC = () => { )} - {curFilter !== 'deleted' && showAction ? ( + {curFilter !== 'deleted' && + (showAction || showActionActivation) ? ( - + {showActionPassword ? ( @@ -291,6 +309,12 @@ const Users: FC = () => { {t('change_role')} ) : null} + {showActionActivation ? ( + handleAction('activation', user)}> + {t('btn_name', { keyPrefix: 'inactive' })} + + ) : null} diff --git a/ui/src/pages/Questions/Ask/index.tsx b/ui/src/pages/Questions/Ask/index.tsx index cdc087ee..0f139d2f 100644 --- a/ui/src/pages/Questions/Ask/index.tsx +++ b/ui/src/pages/Questions/Ask/index.tsx @@ -7,7 +7,7 @@ import dayjs from 'dayjs'; import classNames from 'classnames'; import { isEqual } from 'lodash'; -import { usePageTags, usePromptWithUnload } from '@/hooks'; +import { usePageTags, usePromptWithUnload, useCaptchaModal } from '@/hooks'; import { Editor, EditorRef, TagSelector } from '@/components'; import type * as Type from '@/common/interface'; import { DRAFT_QUESTION_STORAGE_KEY } from '@/common/constants'; @@ -67,7 +67,7 @@ const Ask = () => { const [formData, setFormData] = useState(initFormData); const [immData, setImmData] = useState(initFormData); const [checked, setCheckState] = useState(false); - const [contentChanged, setContentChanged] = useState(false); + const contentChangedRef = useRef(false); const [focusType, setForceType] = useState(''); const [hasDraft, setHasDraft] = useState(false); const resetForm = () => { @@ -102,6 +102,9 @@ const Ask = () => { isEdit ? '' : formData.title.value, ); + const saveCaptcha = useCaptchaModal('question'); + const editCaptcha = useCaptchaModal('edit'); + const removeDraft = () => { saveDraft.save.cancel(); saveDraft.remove(); @@ -144,9 +147,9 @@ const Ask = () => { tags.value.map((v) => v.slug_name), ) ) { - setContentChanged(true); + contentChangedRef.current = true; } else { - setContentChanged(false); + contentChangedRef.current = false; } return; } @@ -167,15 +170,15 @@ const Ask = () => { }, callback: () => setHasDraft(true), }); - setContentChanged(true); + contentChangedRef.current = true; } else { removeDraft(); - setContentChanged(false); + contentChangedRef.current = false; } }, [formData]); usePromptWithUnload({ - when: contentChanged, + when: contentChangedRef.current, }); const { data: revisions = [] } = useQueryRevisions(qid); @@ -241,7 +244,6 @@ const Ask = () => { }; const handleSubmit = async (event: React.FormEvent) => { - setContentChanged(false); event.preventDefault(); event.stopPropagation(); @@ -250,53 +252,80 @@ const Ask = () => { content: formData.content.value, tags: formData.tags.value, }; - if (isEdit) { - modifyQuestion({ - ...params, - id: qid, - edit_summary: formData.edit_summary.value, - }) - .then((res) => { - navigate(pathFactory.questionLanding(qid, params.url_title), { - state: { isReview: res?.wait_for_review }, - }); - }) - .catch((err) => { - if (err.isError) { - const data = handleFormError(err, formData); - setFormData({ ...data }); - } - }); - } else { - let res; - if (checked) { - res = await saveQuestionWidthAnaser({ - ...params, - answer_content: formData.answer_content.value, - }).catch((err) => { - if (err.isError) { - const data = handleFormError(err, formData); - setFormData({ ...data }); - } - }); - } else { - res = await saveQuestion(params).catch((err) => { - if (err.isError) { - const data = handleFormError(err, formData); - setFormData({ ...data }); - } - }); - } - const id = res?.id || res?.question?.id; - if (id) { - if (checked) { - navigate(pathFactory.questionLanding(id, res?.question?.url_title)); - } else { - navigate(pathFactory.questionLanding(id)); + if (isEdit) { + editCaptcha.check(() => { + contentChangedRef.current = false; + const ep = { + ...params, + id: qid, + edit_summary: formData.edit_summary.value, + }; + const imgCode = editCaptcha.getCaptcha(); + if (imgCode.verify) { + ep.captcha_code = imgCode.captcha_code; + ep.captcha_id = imgCode.captcha_id; } - } - removeDraft(); + modifyQuestion(ep) + .then(async (res) => { + await editCaptcha.close(); + navigate(pathFactory.questionLanding(qid, params.url_title), { + state: { isReview: res?.wait_for_review }, + }); + }) + .catch((err) => { + if (err.isError) { + editCaptcha.handleCaptchaError(err.list); + const data = handleFormError(err, formData); + setFormData({ ...data }); + } + }); + }); + } else { + saveCaptcha.check(async () => { + contentChangedRef.current = false; + const imgCode = saveCaptcha.getCaptcha(); + if (imgCode.verify) { + params.captcha_code = imgCode.captcha_code; + params.captcha_id = imgCode.captcha_id; + } + let res; + if (checked) { + res = await saveQuestionWidthAnaser({ + ...params, + answer_content: formData.answer_content.value, + }).catch((err) => { + if (err.isError) { + const captchaErr = saveCaptcha.handleCaptchaError(err.list); + if (!(captchaErr && err.list.length === 1)) { + const data = handleFormError(err, formData); + setFormData({ ...data }); + } + } + }); + } else { + res = await saveQuestion(params).catch((err) => { + if (err.isError) { + const captchaErr = saveCaptcha.handleCaptchaError(err.list); + if (!(captchaErr && err.list.length === 1)) { + const data = handleFormError(err, formData); + setFormData({ ...data }); + } + } + }); + } + + const id = res?.id || res?.question?.id; + if (id) { + await saveCaptcha.close(); + if (checked) { + navigate(pathFactory.questionLanding(id, res?.question?.url_title)); + } else { + navigate(pathFactory.questionLanding(id)); + } + } + removeDraft(); + }); } }; const backPage = () => { @@ -347,6 +376,7 @@ const Ask = () => { {t('form.fields.title.label')} = ({ }, 100); } }, [data.id, answerRef.current]); + if (!data?.id) { return null; } + return (
{data.status === 10 && ( diff --git a/ui/src/pages/Questions/Detail/components/InviteToAnswer/PeopleDropdown.tsx b/ui/src/pages/Questions/Detail/components/InviteToAnswer/PeopleDropdown.tsx index 3b3a7911..94d8eb7c 100644 --- a/ui/src/pages/Questions/Detail/components/InviteToAnswer/PeopleDropdown.tsx +++ b/ui/src/pages/Questions/Detail/components/InviteToAnswer/PeopleDropdown.tsx @@ -130,6 +130,7 @@ const Index: FC = ({ {toggleState ? ( = ({ active={idx === currentIndex} className={idx === 0 ? 'mt-2' : ''}>
- +
{p.display_name} diff --git a/ui/src/pages/Questions/Detail/components/InviteToAnswer/index.tsx b/ui/src/pages/Questions/Detail/components/InviteToAnswer/index.tsx index 41f7eb48..b3c41631 100644 --- a/ui/src/pages/Questions/Detail/components/InviteToAnswer/index.tsx +++ b/ui/src/pages/Questions/Detail/components/InviteToAnswer/index.tsx @@ -8,6 +8,7 @@ import classNames from 'classnames'; import { Avatar } from '@/components'; import { getInviteUser, putInviteUser } from '@/services'; import type * as Type from '@/common/interface'; +import { useCaptchaModal } from '@/hooks'; import PeopleDropdown from './PeopleDropdown'; @@ -22,6 +23,7 @@ const Index: FC = ({ questionId, readOnly = false }) => { const MAX_ASK_NUMBER = 5; const [editing, setEditing] = useState(false); const [users, setUsers] = useState(); + const iaCaptcha = useCaptchaModal('invitation_answer'); const initInviteUsers = () => { if (!questionId) { @@ -60,14 +62,23 @@ const Index: FC = ({ questionId, readOnly = false }) => { const names = users.map((_) => { return _.username; }); - putInviteUser(questionId, names) - .then(() => { - setEditing(false); - }) - .catch((ex) => { - console.log('ex: ', ex); - }); + iaCaptcha.check(() => { + const imgCode: Type.ImgCodeReq = {}; + iaCaptcha.resolveCaptchaReq(imgCode); + putInviteUser(questionId, names, imgCode) + .then(async () => { + await iaCaptcha.close(); + setEditing(false); + }) + .catch((ex) => { + if (ex.isError) { + iaCaptcha.handleCaptchaError(ex.list); + } + console.log('ex: ', ex); + }); + }); }; + useEffect(() => { initInviteUsers(); }, [questionId]); @@ -119,6 +130,7 @@ const Index: FC = ({ questionId, readOnly = false }) => { avatar={user.avatar} size="20" className="rounded-1" + alt={user.display_name} /> {user.display_name} {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events */} @@ -135,7 +147,12 @@ const Index: FC = ({ questionId, readOnly = false }) => { key={user.username} to={`/users/${user.username}`} className="mx-2 my-1 d-inline-flex flex-nowrap"> - + {user.display_name} ); diff --git a/ui/src/pages/Questions/Detail/components/RelatedQuestions/index.tsx b/ui/src/pages/Questions/Detail/components/RelatedQuestions/index.tsx index 5606fb76..e73811ed 100644 --- a/ui/src/pages/Questions/Detail/components/RelatedQuestions/index.tsx +++ b/ui/src/pages/Questions/Detail/components/RelatedQuestions/index.tsx @@ -5,14 +5,12 @@ import { useTranslation } from 'react-i18next'; import { Icon } from '@/components'; import { useSimilarQuestion } from '@/services'; -import { loggedUserInfoStore } from '@/stores'; import { pathFactory } from '@/router/pathFactory'; interface Props { id: string; } const Index: FC = ({ id }) => { - const { user } = loggedUserInfoStore(); const { t } = useTranslation('translation', { keyPrefix: 'related_question', }); @@ -63,11 +61,6 @@ const Index: FC = ({ id }) => { ); })} - {user?.username ? ( - - {t('btn')} - - ) : null} ); }; diff --git a/ui/src/pages/Questions/Detail/components/WriteAnswer/index.tsx b/ui/src/pages/Questions/Detail/components/WriteAnswer/index.tsx index b80e767c..b1d84ec6 100644 --- a/ui/src/pages/Questions/Detail/components/WriteAnswer/index.tsx +++ b/ui/src/pages/Questions/Detail/components/WriteAnswer/index.tsx @@ -5,9 +5,9 @@ import { useTranslation, Trans } from 'react-i18next'; import { marked } from 'marked'; import classNames from 'classnames'; -import { usePromptWithUnload } from '@/hooks'; +import { usePromptWithUnload, useCaptchaModal } from '@/hooks'; import { Editor, Modal, TextArea } from '@/components'; -import { FormDataType } from '@/common/interface'; +import { FormDataType, PostAnswerReq } from '@/common/interface'; import { postAnswer } from '@/services'; import { guard, handleFormError, SaveDraft, storageExpires } from '@/utils'; import { DRAFT_ANSWER_STORAGE_KEY } from '@/common/constants'; @@ -41,6 +41,7 @@ const Index: FC = ({ visible = false, data, callback }) => { const [editorFocusState, setEditorFocusState] = useState(false); const [hasDraft, setHasDraft] = useState(false); const [showTips, setShowTips] = useState(data.loggedUserRank < 100); + const aCaptcha = useCaptchaModal('answer'); usePromptWithUnload({ when: Boolean(formData.content.value), @@ -135,29 +136,40 @@ const Index: FC = ({ visible = false, data, callback }) => { if (!checkValidated()) { return; } - postAnswer({ - question_id: data?.qid, - content: formData.content.value, - html: marked.parse(formData.content.value), - }) - .then((res) => { - setShowEditor(false); - setFormData({ - content: { - value: '', - isInvalid: false, - errorMsg: '', - }, + + aCaptcha.check(() => { + const params: PostAnswerReq = { + question_id: data?.qid, + content: formData.content.value, + html: marked.parse(formData.content.value), + }; + const imgCode = aCaptcha.getCaptcha(); + if (imgCode.verify) { + params.captcha_code = imgCode.captcha_code; + params.captcha_id = imgCode.captcha_id; + } + postAnswer(params) + .then(async (res) => { + await aCaptcha.close(); + setShowEditor(false); + setFormData({ + content: { + value: '', + isInvalid: false, + errorMsg: '', + }, + }); + removeDraft(); + callback?.(res.info); + }) + .catch((ex) => { + if (ex.isError) { + aCaptcha.handleCaptchaError(ex.list); + const stateData = handleFormError(ex, formData); + setFormData({ ...stateData }); + } }); - removeDraft(); - callback?.(res.info); - }) - .catch((ex) => { - if (ex.isError) { - const stateData = handleFormError(ex, formData); - setFormData({ ...stateData }); - } - }); + }); }; const clickBtn = () => { diff --git a/ui/src/pages/Questions/EditAnswer/index.scss b/ui/src/pages/Questions/EditAnswer/index.scss index e9a156cf..64ffeaa7 100644 --- a/ui/src/pages/Questions/EditAnswer/index.scss +++ b/ui/src/pages/Questions/EditAnswer/index.scss @@ -24,13 +24,13 @@ width: 16px; min-height: 50px; resize: vertical; - transform: scale(100, 1); + transform: scale(110, 1); height: 100%; } .resize-bottom + .line { left: 0; width: 100%; - height: 12px; + height: 16px; top: auto; bottom: 0; } diff --git a/ui/src/pages/Questions/EditAnswer/index.tsx b/ui/src/pages/Questions/EditAnswer/index.tsx index 00e6fad1..7f3e3ab2 100644 --- a/ui/src/pages/Questions/EditAnswer/index.tsx +++ b/ui/src/pages/Questions/EditAnswer/index.tsx @@ -7,7 +7,7 @@ import dayjs from 'dayjs'; import classNames from 'classnames'; import { handleFormError, scrollToDocTop } from '@/utils'; -import { usePageTags, usePromptWithUnload } from '@/hooks'; +import { usePageTags, usePromptWithUnload, useCaptchaModal } from '@/hooks'; import { pathFactory } from '@/router/pathFactory'; import { Editor, EditorRef, Icon, htmlRender } from '@/components'; import type * as Type from '@/common/interface'; @@ -51,6 +51,7 @@ const Index = () => { const [formData, setFormData] = useState(initFormData); const [immData, setImmData] = useState(initFormData); const [contentChanged, setContentChanged] = useState(false); + const editCaptcha = useCaptchaModal('edit'); useLayoutEffect(() => { if (data?.info?.content) { @@ -136,36 +137,43 @@ const Index = () => { event.preventDefault(); event.stopPropagation(); + if (!checkValidated()) { return; } - const params: Type.AnswerParams = { - content: formData.content.value, - html: editorRef.current.getHtml(), - question_id: qid, - id: aid, - edit_summary: formData.description.value, - }; - modifyAnswer(params) - .then((res) => { - navigate( - pathFactory.answerLanding({ - questionId: qid, - slugTitle: data?.question?.url_title, - answerId: aid, - }), - { - state: { isReview: res?.wait_for_review }, - }, - ); - }) - .catch((ex) => { - if (ex.isError) { - const stateData = handleFormError(ex, formData); - setFormData({ ...stateData }); - } - }); + editCaptcha.check(() => { + const params: Type.AnswerParams = { + content: formData.content.value, + html: editorRef.current.getHtml(), + question_id: qid, + id: aid, + edit_summary: formData.description.value, + }; + editCaptcha.resolveCaptchaReq(params); + + modifyAnswer(params) + .then(async (res) => { + await editCaptcha.close(); + navigate( + pathFactory.answerLanding({ + questionId: qid, + slugTitle: data?.question?.url_title, + answerId: aid, + }), + { + state: { isReview: res?.wait_for_review }, + }, + ); + }) + .catch((ex) => { + if (ex.isError) { + editCaptcha.handleCaptchaError(ex.list); + const stateData = handleFormError(ex, formData); + setFormData({ ...stateData }); + } + }); + }); }; const handleSelectedRevision = (e) => { const index = e.target.value; @@ -198,7 +206,7 @@ const Index = () => {
{ style={{ maxHeight: questionContentRef?.current?.scrollHeight }} />
- +
diff --git a/ui/src/pages/Search/index.tsx b/ui/src/pages/Search/index.tsx index 4ea10ee3..7443d05a 100644 --- a/ui/src/pages/Search/index.tsx +++ b/ui/src/pages/Search/index.tsx @@ -1,10 +1,12 @@ import { Row, Col, ListGroup } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; import { useSearchParams } from 'react-router-dom'; +import { useEffect, useState } from 'react'; -import { usePageTags } from '@/hooks'; +import { usePageTags, useCaptchaModal } from '@/hooks'; import { Pagination } from '@/components'; -import { useSearch } from '@/services'; +import { getSearchResult } from '@/services'; +import type { SearchParams, SearchRes } from '@/common/interface'; import { Head, @@ -21,15 +23,52 @@ const Index = () => { const page = searchParams.get('page') || 1; const q = searchParams.get('q') || ''; const order = searchParams.get('order') || 'active'; - - const { data, isLoading } = useSearch({ - q, - order, - page: Number(page), - size: 20, + const [isLoading, setIsLoading] = useState(false); + const [data, setData] = useState({ + count: 0, + list: [], + extra: null, }); - const { count = 0, list = [], extra = null } = data || {}; + + const searchCaptcha = useCaptchaModal('search'); + + const doSearch = () => { + setIsLoading(true); + const params: SearchParams = { + q, + order, + page: Number(page), + size: 20, + }; + + const captcha = searchCaptcha.getCaptcha(); + if (captcha?.verify) { + params.captcha_id = captcha.captcha_id; + params.captcha_code = captcha.captcha_code; + } + + getSearchResult(params) + .then((resp) => { + searchCaptcha.close(); + setData(resp); + }) + .catch((err) => { + if (err.isError) { + searchCaptcha.handleCaptchaError(err.list); + } + }) + .finally(() => { + setIsLoading(false); + }); + }; + + useEffect(() => { + searchCaptcha.check(() => { + doSearch(); + }); + }, [q, order, page]); + let pageTitle = t('search', { keyPrefix: 'page_title' }); if (q) { pageTitle = `${t('posts_containing', { keyPrefix: 'page_title' })} '${q}'`; @@ -37,6 +76,7 @@ const Index = () => { usePageTags({ title: pageTitle, }); + return ( diff --git a/ui/src/pages/Tags/Create/index.tsx b/ui/src/pages/Tags/Create/index.tsx index 4cf3716f..8b846e9c 100644 --- a/ui/src/pages/Tags/Create/index.tsx +++ b/ui/src/pages/Tags/Create/index.tsx @@ -147,6 +147,7 @@ const Index = () => { {t('form.fields.display_name.label')} { {t('form.fields.slug_name.label')} { diff --git a/ui/src/pages/Users/AccountForgot/components/sendEmail.tsx b/ui/src/pages/Users/AccountForgot/components/sendEmail.tsx index 8c6aa161..323d9113 100644 --- a/ui/src/pages/Users/AccountForgot/components/sendEmail.tsx +++ b/ui/src/pages/Users/AccountForgot/components/sendEmail.tsx @@ -1,22 +1,19 @@ -import { FC, memo, useEffect, useState } from 'react'; +import { FC, memo, useState } from 'react'; import { Form, Button } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; -import type { - ImgCodeRes, - PasswordResetReq, - FormDataType, -} from '@/common/interface'; -import { resetPassword, checkImgCode } from '@/services'; -import { PicAuthCodeModal } from '@/components/Modal'; +import type { PasswordResetReq, FormDataType } from '@/common/interface'; +import { resetPassword } from '@/services'; import { handleFormError } from '@/utils'; +import { useCaptchaModal } from '@/hooks'; interface IProps { - visible: boolean; + // eslint-disable-next-line react/no-unused-prop-types + visible?: boolean; callback: (param: number, email: string) => void; } -const Index: FC = ({ visible = false, callback }) => { +const Index: FC = ({ callback }) => { const { t } = useTranslation('translation', { keyPrefix: 'account_forgot' }); const [formData, setFormData] = useState({ e_mail: { @@ -24,26 +21,9 @@ const Index: FC = ({ visible = false, callback }) => { isInvalid: false, errorMsg: '', }, - captcha_code: { - value: '', - isInvalid: false, - errorMsg: '', - }, }); - const [imgCode, setImgCode] = useState({ - captcha_id: '', - captcha_img: '', - verify: false, - }); - const [showModal, setModalState] = useState(false); - const getImgCode = () => { - checkImgCode({ - action: 'find_pass', - }).then((res) => { - setImgCode(res); - }); - }; + const emailCaptcha = useCaptchaModal('email'); const handleChange = (params: FormDataType) => { setFormData({ ...formData, ...params }); @@ -73,27 +53,24 @@ const Index: FC = ({ visible = false, callback }) => { const params: PasswordResetReq = { e_mail: formData.e_mail.value, }; - if (imgCode.verify) { - params.captcha_code = formData.captcha_code.value; - params.captcha_id = imgCode.captcha_id; + + const captcha = emailCaptcha.getCaptcha(); + if (captcha.verify) { + params.captcha_code = captcha.captcha_code; + params.captcha_id = captcha.captcha_id; } resetPassword(params) - .then(() => { + .then(async () => { + await emailCaptcha.close(); callback?.(2, formData.e_mail.value); - setModalState(false); }) .catch((err) => { if (err.isError) { + emailCaptcha.handleCaptchaError(err.list); const data = handleFormError(err, formData); - if (!err.list.find((v) => v.error_field.indexOf('captcha') >= 0)) { - setModalState(false); - } setFormData({ ...data }); } - }) - .finally(() => { - getImgCode(); }); }; @@ -105,64 +82,41 @@ const Index: FC = ({ visible = false, callback }) => { return; } - if (imgCode.verify) { - setModalState(true); - return; - } - - sendEmail(); + emailCaptcha.check(() => { + sendEmail(); + }); }; - useEffect(() => { - if (visible) { - getImgCode(); - } - }, [visible]); - return ( - <> -
- - {t('email.label')} - { - handleChange({ - e_mail: { - value: e.target.value, - isInvalid: false, - errorMsg: '', - }, - }); - }} - /> - - {formData.e_mail.errorMsg} - - + + + {t('email.label')} + { + handleChange({ + e_mail: { + value: e.target.value, + isInvalid: false, + errorMsg: '', + }, + }); + }} + /> + + {formData.e_mail.errorMsg} + + -
- -
-
- - setModalState(false)} - /> - +
+ +
+ ); }; diff --git a/ui/src/pages/Users/ChangeEmail/components/sendEmail.tsx b/ui/src/pages/Users/ChangeEmail/components/sendEmail.tsx index 6f884ca0..7143c099 100644 --- a/ui/src/pages/Users/ChangeEmail/components/sendEmail.tsx +++ b/ui/src/pages/Users/ChangeEmail/components/sendEmail.tsx @@ -1,17 +1,13 @@ -import { FC, memo, useEffect, useState } from 'react'; +import { FC, memo, useState } from 'react'; import { Form, Button } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; -import type { - ImgCodeRes, - PasswordResetReq, - FormDataType, -} from '@/common/interface'; +import type { PasswordResetReq, FormDataType } from '@/common/interface'; import { loggedUserInfoStore } from '@/stores'; -import { changeEmail, checkImgCode } from '@/services'; -import { PicAuthCodeModal } from '@/components/Modal'; +import { changeEmail } from '@/services'; import { handleFormError } from '@/utils'; +import { useCaptchaModal } from '@/hooks'; const Index: FC = () => { const { t } = useTranslation('translation', { keyPrefix: 'change_email' }); @@ -21,28 +17,12 @@ const Index: FC = () => { isInvalid: false, errorMsg: '', }, - captcha_code: { - value: '', - isInvalid: false, - errorMsg: '', - }, }); - const [imgCode, setImgCode] = useState({ - captcha_id: '', - captcha_img: '', - verify: false, - }); - const [showModal, setModalState] = useState(false); + const navigate = useNavigate(); const { user: userInfo, update: updateUser } = loggedUserInfoStore(); - const getImgCode = () => { - checkImgCode({ - action: 'e_mail', - }).then((res) => { - setImgCode(res); - }); - }; + const emailCaptcha = useCaptchaModal('email'); const handleChange = (params: FormDataType) => { setFormData({ ...formData, ...params }); @@ -72,28 +52,25 @@ const Index: FC = () => { const params: PasswordResetReq = { e_mail: formData.e_mail.value, }; + const imgCode = emailCaptcha.getCaptcha(); if (imgCode.verify) { - params.captcha_code = formData.captcha_code.value; + params.captcha_code = imgCode.captcha_code; params.captcha_id = imgCode.captcha_id; } + changeEmail(params) - .then(() => { + .then(async () => { + await emailCaptcha.close(); userInfo.e_mail = formData.e_mail.value; updateUser(userInfo); navigate('/users/login', { replace: true }); - setModalState(false); }) .catch((err) => { if (err.isError) { + emailCaptcha.handleCaptchaError(err.list); const data = handleFormError(err, formData); - if (!err.list.find((v) => v.error_field.indexOf('captcha') >= 0)) { - setModalState(false); - } setFormData({ ...data }); } - }) - .finally(() => { - getImgCode(); }); }; @@ -104,69 +81,48 @@ const Index: FC = () => { return; } - if (imgCode.verify) { - setModalState(true); - return; - } - - sendEmail(); + emailCaptcha.check(() => { + sendEmail(); + }); }; const goBack = () => { navigate('/users/login?status=inactive', { replace: true }); }; - useEffect(() => { - getImgCode(); - }, []); - return ( - <> -
- - {t('email.label')} - { - handleChange({ - e_mail: { - value: e.target.value, - isInvalid: false, - errorMsg: '', - }, - }); - }} - /> - - {formData.e_mail.errorMsg} - - + + + {t('email.label')} + { + handleChange({ + e_mail: { + value: e.target.value, + isInvalid: false, + errorMsg: '', + }, + }); + }} + /> + + {formData.e_mail.errorMsg} + + -
- - -
-
- - setModalState(false)} - /> - +
+ + +
+ ); }; diff --git a/ui/src/pages/Users/Login/index.tsx b/ui/src/pages/Users/Login/index.tsx index a6bedd59..69981843 100644 --- a/ui/src/pages/Users/Login/index.tsx +++ b/ui/src/pages/Users/Login/index.tsx @@ -3,12 +3,8 @@ import { Container, Form, Button, Col } from 'react-bootstrap'; import { Link, useNavigate, useSearchParams } from 'react-router-dom'; import { Trans, useTranslation } from 'react-i18next'; -import { usePageTags } from '@/hooks'; -import type { - LoginReqParams, - ImgCodeRes, - FormDataType, -} from '@/common/interface'; +import { usePageTags, useCaptchaModal } from '@/hooks'; +import type { LoginReqParams, FormDataType } from '@/common/interface'; import { Unactivate, WelcomeTitle, PluginRender } from '@/components'; import { loggedUserInfoStore, @@ -16,14 +12,12 @@ import { userCenterStore, } from '@/stores'; import { floppyNavigation, guard, handleFormError, userCenter } from '@/utils'; -import { login, checkImgCode, UcAgent } from '@/services'; -import { PicAuthCodeModal } from '@/components/Modal'; +import { login, UcAgent } from '@/services'; const Index: React.FC = () => { const { t } = useTranslation('translation', { keyPrefix: 'login' }); const navigate = useNavigate(); const [searchParams] = useSearchParams(); - const [refresh, setRefresh] = useState(0); const { user: storeUser, update: updateUser } = loggedUserInfoStore((_) => _); const loginSetting = loginSettingStore((state) => state.login); const ucAgent = userCenterStore().agent; @@ -45,34 +39,15 @@ const Index: React.FC = () => { isInvalid: false, errorMsg: '', }, - captcha_code: { - value: '', - isInvalid: false, - errorMsg: '', - }, }); - const [imgCode, setImgCode] = useState({ - captcha_id: '', - captcha_img: '', - verify: false, - }); - const [showModal, setModalState] = useState(false); + const [step, setStep] = useState(1); const handleChange = (params: FormDataType) => { setFormData({ ...formData, ...params }); }; - const getImgCode = () => { - if (!canOriginalLogin) { - return; - } - checkImgCode({ - action: 'login', - }).then((res) => { - setImgCode(res); - }); - }; + const passwordCaptcha = useCaptchaModal('password'); const checkValidated = (): boolean => { let bol = true; @@ -110,34 +85,31 @@ const Index: React.FC = () => { e_mail: formData.e_mail.value, pass: formData.pass.value, }; - if (imgCode.verify) { - params.captcha_code = formData.captcha_code.value; - params.captcha_id = imgCode.captcha_id; + + const captcha = passwordCaptcha.getCaptcha(); + if (captcha?.verify) { + params.captcha_code = captcha.captcha_code; + params.captcha_id = captcha.captcha_id; } login(params) - .then((res) => { + .then(async (res) => { + await passwordCaptcha.close(); updateUser(res); const userStat = guard.deriveLoginState(); if (userStat.isNotActivated) { // inactive setStep(2); - setRefresh((pre) => pre + 1); } else { guard.handleLoginRedirect(navigate); } - - setModalState(false); }) .catch((err) => { if (err.isError) { const data = handleFormError(err, formData); - if (!err.list.find((v) => v.error_field.indexOf('captcha') >= 0)) { - setModalState(false); - } setFormData({ ...data }); + passwordCaptcha.handleCaptchaError(err.list); } - setRefresh((pre) => pre + 1); }); }; @@ -149,18 +121,11 @@ const Index: React.FC = () => { return; } - if (imgCode.verify) { - setModalState(true); - return; - } - - handleLogin(); + passwordCaptcha.check(() => { + handleLogin(); + }); }; - useEffect(() => { - getImgCode(); - }, [refresh]); - useEffect(() => { const isInactive = searchParams.get('status'); @@ -168,6 +133,7 @@ const Index: React.FC = () => { setStep(2); } }, []); + usePageTags({ title: t('login', { keyPrefix: 'page_title' }), }); @@ -263,18 +229,6 @@ const Index: React.FC = () => { ) : null} {step === 2 && } - - setModalState(false)} - /> ); }; diff --git a/ui/src/pages/Users/Personal/components/TopList/index.tsx b/ui/src/pages/Users/Personal/components/TopList/index.tsx index 42210a0f..c33fb51b 100644 --- a/ui/src/pages/Users/Personal/components/TopList/index.tsx +++ b/ui/src/pages/Users/Personal/components/TopList/index.tsx @@ -18,6 +18,7 @@ const Index: FC = ({ data, type }) => { className="mb-2" key={type === 'answer' ? item.answer_id : item.question_id}> = ({ data, type }) => { {type === 'answer' ? item.question_info.title : item.title} -
+
{item.vote_count} {t('votes', { keyPrefix: 'counts' })} @@ -41,7 +42,7 @@ const Index: FC = ({ data, type }) => {
{type === 'question' && (
0 ? 'text-success' : '' }`}> {Number(item.accepted_answer_id) > 0 ? ( @@ -58,7 +59,7 @@ const Index: FC = ({ data, type }) => { )} {type === 'answer' && item.accepted === 2 && ( -
+
{t('accepted')}
diff --git a/ui/src/pages/Users/Personal/components/UserInfo/index.tsx b/ui/src/pages/Users/Personal/components/UserInfo/index.tsx index 2e76b206..9c88d420 100644 --- a/ui/src/pages/Users/Personal/components/UserInfo/index.tsx +++ b/ui/src/pages/Users/Personal/components/UserInfo/index.tsx @@ -39,10 +39,20 @@ const Index: FC = ({ data }) => {
{data?.status !== 'deleted' ? ( - + ) : ( - + )}
diff --git a/ui/src/pages/Users/Register/components/SignUpForm/index.tsx b/ui/src/pages/Users/Register/components/SignUpForm/index.tsx index 6abbe142..0340d26b 100644 --- a/ui/src/pages/Users/Register/components/SignUpForm/index.tsx +++ b/ui/src/pages/Users/Register/components/SignUpForm/index.tsx @@ -1,17 +1,11 @@ -import React, { FormEvent, MouseEvent, useEffect, useState } from 'react'; +import React, { FormEvent, MouseEvent, useState } from 'react'; import { Form, Button } from 'react-bootstrap'; import { Link } from 'react-router-dom'; import { Trans, useTranslation } from 'react-i18next'; -import { PicAuthCodeModal } from '@/components/Modal'; -import { ImgCodeRes } from '@/common/interface'; +import { useCaptchaModal } from '@/hooks'; import type { FormDataType, RegisterReqParams } from '@/common/interface'; -import { - register, - getRegisterCaptcha, - useLegalTos, - useLegalPrivacy, -} from '@/services'; +import { register, useLegalTos, useLegalPrivacy } from '@/services'; import userStore from '@/stores/loggedUserInfo'; import { handleFormError } from '@/utils'; @@ -37,54 +31,18 @@ const Index: React.FC = ({ callback }) => { isInvalid: false, errorMsg: '', }, - captcha_code: { - value: '', - isInvalid: false, - errorMsg: '', - }, }); - const updateUser = userStore((state) => state.update); - const [imgCode, setImgCode] = useState({ - captcha_id: '', - captcha_img: '', - verify: false, - }); - const [showModal, setModalState] = useState(false); - const getImgCode = () => { - getRegisterCaptcha().then((res) => { - setImgCode(res); - }); - }; + const updateUser = userStore((state) => state.update); + const emailCaptcha = useCaptchaModal('email'); + const handleChange = (params: FormDataType) => { setFormData({ ...formData, ...params }); }; const checkValidated = (): boolean => { let bol = true; - const { name, e_mail, pass } = formData; - if (!name.value) { - bol = false; - formData.name = { - value: '', - isInvalid: true, - errorMsg: t('name.msg.empty'), - }; - } else if (/[^a-z0-9\-._]/.test(name.value)) { - bol = false; - formData.name = { - value: name.value, - isInvalid: true, - errorMsg: t('name.msg.character'), - }; - } else if ([...name.value].length > 30) { - bol = false; - formData.name = { - value: name.value, - isInvalid: true, - errorMsg: t('name.msg.range'), - }; - } + const { e_mail, pass } = formData; if (!e_mail.value) { bol = false; @@ -108,6 +66,7 @@ const Index: React.FC = ({ callback }) => { }); return bol; }; + const { data: tos } = useLegalTos(); const { data: privacy } = useLegalPrivacy(); const argumentClick = (evt: MouseEvent, type: 'tos' | 'privacy') => { @@ -139,22 +98,22 @@ const Index: React.FC = ({ callback }) => { pass: formData.pass.value, }; - if (imgCode.verify) { - reqParams.captcha_code = formData.captcha_code.value; - reqParams.captcha_id = imgCode.captcha_id; + const captcha = emailCaptcha.getCaptcha(); + if (captcha?.verify) { + reqParams.captcha_code = captcha.captcha_code; + reqParams.captcha_id = captcha.captcha_id; } + register(reqParams) .then((res) => { + emailCaptcha.close(); updateUser(res); - setModalState(false); callback(); }) .catch((err) => { if (err.isError) { + emailCaptcha.handleCaptchaError(err.list); const data = handleFormError(err, formData); - if (!err.list.find((v) => v.error_field.indexOf('captcha') >= 0)) { - setModalState(false); - } setFormData({ ...data }); } }); @@ -166,15 +125,11 @@ const Index: React.FC = ({ callback }) => { if (!checkValidated()) { return; } - if (imgCode.verify) { - setModalState(true); - return; - } - handleRegister(); + emailCaptcha.check(() => { + handleRegister(); + }); }; - useEffect(() => { - getImgCode(); - }, []); + return ( <>
@@ -276,23 +231,6 @@ const Index: React.FC = ({ callback }) => { .
-
- - Already have an account? Log in - -
- - setModalState(false)} - /> ); }; diff --git a/ui/src/pages/Users/Register/index.tsx b/ui/src/pages/Users/Register/index.tsx index efc10351..be646ecc 100644 --- a/ui/src/pages/Users/Register/index.tsx +++ b/ui/src/pages/Users/Register/index.tsx @@ -1,25 +1,34 @@ import React, { useState } from 'react'; import { Container, Col } from 'react-bootstrap'; -import { useTranslation } from 'react-i18next'; +import { Trans, useTranslation } from 'react-i18next'; +import { Link } from 'react-router-dom'; import { usePageTags } from '@/hooks'; import { Unactivate, WelcomeTitle, PluginRender } from '@/components'; import { guard } from '@/utils'; +import { loginSettingStore } from '@/stores'; import SignUpForm from './components/SignUpForm'; const Index: React.FC = () => { const [showForm, setShowForm] = useState(true); const { t } = useTranslation('translation', { keyPrefix: 'login' }); + const loginSetting = loginSettingStore((state) => state.login); const onStep = () => { setShowForm((bol) => !bol); }; usePageTags({ title: t('sign_up', { keyPrefix: 'page_title' }), }); + if (!guard.singUpAgent().ok) { return null; } + + const showSignupForm = + loginSetting?.allow_new_registrations && + loginSetting.allow_email_registrations; + return ( @@ -27,7 +36,12 @@ const Index: React.FC = () => { {showForm ? ( - + {showSignupForm ? : null} +
+ + Already have an account? Log in + +
) : ( diff --git a/ui/src/pages/Users/Settings/Account/components/ModifyEmail/index.tsx b/ui/src/pages/Users/Settings/Account/components/ModifyEmail/index.tsx index a934383d..3c5fdee1 100644 --- a/ui/src/pages/Users/Settings/Account/components/ModifyEmail/index.tsx +++ b/ui/src/pages/Users/Settings/Account/components/ModifyEmail/index.tsx @@ -3,22 +3,15 @@ import { Form, Button } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; import type * as Type from '@/common/interface'; -import { useToast } from '@/hooks'; -import { getLoggedUserInfo, changeEmail, checkImgCode } from '@/services'; +import { useToast, useCaptchaModal } from '@/hooks'; +import { getLoggedUserInfo, changeEmail } from '@/services'; import { handleFormError } from '@/utils'; -import { PicAuthCodeModal } from '@/components'; const Index: FC = () => { const { t } = useTranslation('translation', { keyPrefix: 'settings.account', }); const [step, setStep] = useState(1); - const [showModal, setModalState] = useState(false); - const [imgCode, setImgCode] = useState({ - captcha_id: '', - captcha_img: '', - verify: false, - }); const [formData, setFormData] = useState({ e_mail: { value: '', @@ -30,28 +23,17 @@ const Index: FC = () => { isInvalid: false, errorMsg: '', }, - captcha_code: { - value: '', - isInvalid: false, - errorMsg: '', - }, }); const [userInfo, setUserInfo] = useState(); const toast = useToast(); + const emailCaptcha = useCaptchaModal('edit_userinfo'); + useEffect(() => { getLoggedUserInfo().then((resp) => { setUserInfo(resp); }); }, []); - const getImgCode = () => { - checkImgCode({ - action: 'e_mail', - }).then((res) => { - setImgCode(res); - }); - }; - const handleChange = (params: Type.FormDataType) => { setFormData({ ...formData, ...params }); }; @@ -95,11 +77,6 @@ const Index: FC = () => { isInvalid: false, errorMsg: '', }, - captcha_code: { - value: '', - isInvalid: false, - errorMsg: '', - }, }); }; @@ -112,14 +89,15 @@ const Index: FC = () => { pass: formData.pass.value, }; + const imgCode = emailCaptcha.getCaptcha(); if (imgCode.verify) { - params.captcha_code = formData.captcha_code.value; + params.captcha_code = imgCode.captcha_code; params.captcha_id = imgCode.captcha_id; } changeEmail(params) - .then(() => { + .then(async () => { + await emailCaptcha.close(); setStep(1); - setModalState(false); toast.onShow({ msg: t('change_email_info'), variant: 'warning', @@ -128,15 +106,10 @@ const Index: FC = () => { }) .catch((err) => { if (err.isError) { + emailCaptcha.handleCaptchaError(err.list); const data = handleFormError(err, formData); setFormData({ ...data }); - if (!err.list.find((v) => v.error_field.indexOf('captcha') >= 0)) { - setModalState(false); - } } - }) - .finally(() => { - getImgCode(); }); }; @@ -147,11 +120,9 @@ const Index: FC = () => { return; } - if (imgCode.verify) { - setModalState(true); - return; - } - postEmail(); + emailCaptcha.check(() => { + postEmail(); + }); }; return ( @@ -174,7 +145,6 @@ const Index: FC = () => { variant="outline-secondary" onClick={() => { setStep(2); - getImgCode(); }}> {t('change_email_btn')} @@ -240,18 +210,6 @@ const Index: FC = () => {
)} - - setModalState(false)} - />
); }; diff --git a/ui/src/pages/Users/Settings/Account/components/ModifyPass/index.tsx b/ui/src/pages/Users/Settings/Account/components/ModifyPass/index.tsx index a06cb987..3caf7c76 100644 --- a/ui/src/pages/Users/Settings/Account/components/ModifyPass/index.tsx +++ b/ui/src/pages/Users/Settings/Account/components/ModifyPass/index.tsx @@ -4,12 +4,11 @@ import { useTranslation } from 'react-i18next'; import classname from 'classnames'; -import { useToast } from '@/hooks'; -import type { FormDataType, ImgCodeRes } from '@/common/interface'; -import { modifyPassword, checkImgCode } from '@/services'; +import { useToast, useCaptchaModal } from '@/hooks'; +import type { FormDataType } from '@/common/interface'; +import { modifyPassword } from '@/services'; import { handleFormError } from '@/utils'; import { loggedUserInfoStore } from '@/stores'; -import { PicAuthCodeModal } from '@/components'; const Index: FC = () => { const { t } = useTranslation('translation', { @@ -35,20 +34,8 @@ const Index: FC = () => { errorMsg: '', }, }); - const [showModal, setModalState] = useState(false); - const [imgCode, setImgCode] = useState({ - captcha_id: '', - captcha_img: '', - verify: false, - }); - const getImgCode = () => { - checkImgCode({ - action: 'modify_pass', - }).then((res) => { - setImgCode(res); - }); - }; + const infoCaptcha = useCaptchaModal('edit_userinfo'); const handleFormState = () => { setFormState((pre) => !pre); @@ -128,13 +115,14 @@ const Index: FC = () => { pass: formData.pass.value, }; + const imgCode = infoCaptcha.getCaptcha(); if (imgCode.verify) { - params.captcha_code = formData.captcha_code.value; + params.captcha_code = imgCode.captcha_code; params.captcha_id = imgCode.captcha_id; } modifyPassword(params) - .then(() => { - setModalState(false); + .then(async () => { + await infoCaptcha.close(); toast.onShow({ msg: t('update_password', { keyPrefix: 'toast' }), variant: 'success', @@ -143,15 +131,10 @@ const Index: FC = () => { }) .catch((err) => { if (err.isError) { + infoCaptcha.handleCaptchaError(err.list); const data = handleFormError(err, formData); - if (!err.list.find((v) => v.error_field.indexOf('captcha') >= 0)) { - setModalState(false); - } setFormData({ ...data }); } - }) - .finally(() => { - getImgCode(); }); }; @@ -162,11 +145,9 @@ const Index: FC = () => { return; } - if (imgCode.verify) { - setModalState(true); - return; - } - postModifyPass(); + infoCaptcha.check(() => { + postModifyPass(); + }); }; return ( @@ -262,24 +243,11 @@ const Index: FC = () => { type="submit" onClick={() => { handleFormState(); - getImgCode(); }}> {t('change_pass_btn')} )} - - setModalState(false)} - />
); }; diff --git a/ui/src/pages/Users/Settings/Profile/index.tsx b/ui/src/pages/Users/Settings/Profile/index.tsx index 4264e9d9..b3359e92 100644 --- a/ui/src/pages/Users/Settings/Profile/index.tsx +++ b/ui/src/pages/Users/Settings/Profile/index.tsx @@ -353,6 +353,7 @@ const Index: React.FC = () => { count > 0 ? `&t=${new Date().valueOf()}` : '' }`} className="me-3 rounded" + alt={formData.display_name.value} /> {t('avatar.gravatar_text')} @@ -381,6 +382,7 @@ const Index: React.FC = () => { searchStr="s=256" avatar={formData.avatar.custom} className="me-2 bg-gray-300 " + alt={formData.display_name.value} /> { )} {formData.avatar.type === 'default' && ( - + )}
diff --git a/ui/src/pages/Users/index.tsx b/ui/src/pages/Users/index.tsx index ab59853e..d2cb9e82 100644 --- a/ui/src/pages/Users/index.tsx +++ b/ui/src/pages/Users/index.tsx @@ -55,10 +55,13 @@ const Users = () => { size="48px" avatar={user?.avatar} searchStr="s=96" + alt={user.display_name} />
- + {user.display_name}
diff --git a/ui/src/router/pathFactory.ts b/ui/src/router/pathFactory.ts index acf6b690..01ffb529 100644 --- a/ui/src/router/pathFactory.ts +++ b/ui/src/router/pathFactory.ts @@ -1,22 +1,20 @@ -import urlcat from 'urlcat'; - import { seoSettingStore } from '@/stores'; const tagLanding = (slugName: string) => { - if (!slugName) { - return '/tags'; - } - return urlcat('/tags/:slugName', { slugName }); + const r = slugName ? `/tags/${slugName}` : '/tags'; + return r; }; + const tagInfo = (slugName: string) => { - if (!slugName) { - return '/tags'; - } - return urlcat('/tags/:slugName/info', { slugName }); + const r = slugName ? `/tags/${slugName}/info` : '/tags'; + return r; }; + const tagEdit = (tagId: string) => { - return urlcat('/tags/:tagId/edit', { tagId }); + const r = `/tags/${tagId}/edit`; + return r; }; + const questionLanding = (questionId: string, slugTitle: string = '') => { const { seo } = seoSettingStore.getState(); if (!questionId) { @@ -24,14 +22,12 @@ const questionLanding = (questionId: string, slugTitle: string = '') => { } // @ts-ignore if (/[13]/.test(seo.permalink) && slugTitle) { - return urlcat('/questions/:questionId/:slugPermalink', { - questionId, - slugPermalink: slugTitle, - }); + return `/questions/${questionId}/${slugTitle}`; } - return urlcat('/questions/:questionId', { questionId }); + return `/questions/${questionId}`; }; + const answerLanding = (params: { questionId: string; slugTitle?: string; @@ -41,9 +37,7 @@ const answerLanding = (params: { params.questionId, params.slugTitle, ); - return urlcat(`${questionLandingUrl}/:answerId`, { - answerId: params.answerId, - }); + return `${questionLandingUrl}/${params.answerId}`; }; export const pathFactory = { diff --git a/ui/src/services/admin/users.ts b/ui/src/services/admin/users.ts index e70440fd..17d83afb 100644 --- a/ui/src/services/admin/users.ts +++ b/ui/src/services/admin/users.ts @@ -44,3 +44,21 @@ export const updateUserPassword = (params: { }) => { return request.put('/answer/admin/api/user/password', params); }; + +export const getUserActivation = (userId: string) => { + const apiUrl = `/answer/admin/api/user/activation`; + return request.get<{ + activation_url: string; + }>(apiUrl, { + params: { + user_id: userId, + }, + }); +}; + +export const postUserActivation = (userId: string) => { + const apiUrl = `/answer/admin/api/user/activation`; + return request.post(apiUrl, { + user_id: userId, + }); +}; diff --git a/ui/src/services/client/question.ts b/ui/src/services/client/question.ts index db66462a..92cc5504 100644 --- a/ui/src/services/client/question.ts +++ b/ui/src/services/client/question.ts @@ -61,10 +61,15 @@ export const getInviteUser = (questionId: string) => { }); }; -export const putInviteUser = (questionId: string, users: string[]) => { +export const putInviteUser = ( + questionId: string, + users: string[], + imgCode: Type.ImgCodeReq = {}, +) => { const apiUrl = '/answer/api/v1/question/invite'; return request.put(apiUrl, { id: questionId, invite_user: users, + ...imgCode, }); }; diff --git a/ui/src/services/client/search.ts b/ui/src/services/client/search.ts index 8d380294..ce6e9ad4 100644 --- a/ui/src/services/client/search.ts +++ b/ui/src/services/client/search.ts @@ -1,20 +1,10 @@ -import useSWR from 'swr'; -import qs from 'qs'; - import request from '@/utils/request'; import type * as Type from '@/common/interface'; -export const useSearch = (params?: Type.SearchParams) => { +export const getSearchResult = (params?: Type.SearchParams) => { const apiUrl = '/answer/api/v1/search'; - const queryParams = qs.stringify(params, { skipNulls: true }); - const { data, error, mutate } = useSWR( - params?.q ? `${apiUrl}?${queryParams}` : null, - request.instance.get, - ); - return { - data, - isLoading: !data && !error, - error, - mutate, - }; + + return request.get(apiUrl, { + params, + }); }; diff --git a/ui/src/services/common.ts b/ui/src/services/common.ts index cf750a52..bf44dc49 100644 --- a/ui/src/services/common.ts +++ b/ui/src/services/common.ts @@ -60,9 +60,10 @@ export const updateComment = (params) => { return request.put('/answer/api/v1/comment', params); }; -export const deleteComment = (id) => { +export const deleteComment = (id, imgCode: Type.ImgCodeReq = {}) => { return request.delete('/answer/api/v1/comment', { comment_id: id, + ...imgCode, }); }; @@ -102,19 +103,10 @@ export const register = (params: Type.RegisterReqParams) => { return request.post('/answer/api/v1/user/register/email', params); }; -export const getRegisterCaptcha = () => { - const apiUrl = '/answer/api/v1/user/register/captcha'; - return request.get(apiUrl); -}; - export const logout = () => { return request.get('/answer/api/v1/user/logout'); }; -export const verifyEmail = (code: string) => { - return request.get(`/answer/api/v1/email/verify?code=${code}`); -}; - export const resendEmail = (params?: Type.ImgCodeReq) => { params = qs.parse( qs.stringify(params, { @@ -134,19 +126,19 @@ export const getLoggedUserInfo = (config = { passingError: false }) => { return request.get('/answer/api/v1/user/info', config); }; -export const modifyPassword = (params: Type.ModifyPasswordReq) => { - return request.put('/answer/api/v1/user/password', params); -}; - export const modifyUserInfo = (params: Type.ModifyUserReq) => { return request.put('/answer/api/v1/user/info', params); }; +export const modifyPassword = (params: Type.ModifyPasswordReq) => { + return request.put('/answer/api/v1/user/password', params); +}; + export const resetPassword = (params: Type.PasswordResetReq) => { return request.post('/answer/api/v1/user/password/reset', params); }; -export const replacementPassword = (params: { code: string; pass: string }) => { +export const replacementPassword = (params: Type.PasswordReplaceReq) => { return request.post('/answer/api/v1/user/password/replacement', params); }; @@ -154,10 +146,13 @@ export const activateAccount = (code: string) => { return request.post(`/answer/api/v1/user/email/verification`, { code }); }; -export const checkImgCode = (params: Type.CheckImgReq) => { - return request.get( - `/answer/api/v1/user/action/record?${qs.stringify(params)}`, - ); +export const checkImgCode = (k: Type.CaptchaKey) => { + const apiUrl = `/answer/api/v1/user/action/record`; + return request.get(apiUrl, { + params: { + action: k, + }, + }); }; export const setNotice = (params: Type.SetNoticeReq) => { @@ -189,7 +184,7 @@ export const bookmark = (params: { group_id: string; object_id: string }) => { }; export const postVote = ( - params: { object_id: string; is_cancel: boolean }, + params: { object_id: string; is_cancel: boolean } & Type.ImgCodeReq, type: 'down' | 'up', ) => { return request.post(`/answer/api/v1/vote/${type}`, params); @@ -224,20 +219,30 @@ export const reportList = ({ return request.get(`${api}?object_type=${type}&action=${action}`); }; -export const postReport = (params: { - source: Type.ReportType; - content: string; - object_id: string; - report_type: number; -}) => { +export const postReport = ( + params: { + source: Type.ReportType; + content: string; + object_id: string; + report_type: number; + } & Type.ImgCodeReq, +) => { return request.post('/answer/api/v1/report', params); }; -export const deleteQuestion = (params: { id: string }) => { +export const deleteQuestion = (params: { + id: string; + captcha_code?: string; + captcha_id?: string; +}) => { return request.delete('/answer/api/v1/question', params); }; -export const deleteAnswer = (params: { id: string }) => { +export const deleteAnswer = (params: { + id: string; + captcha_code?: string; + captcha_id?: string; +}) => { return request.delete('/answer/api/v1/answer', params); }; diff --git a/ui/src/stores/sideNav.ts b/ui/src/stores/sideNav.ts index 3f9a2f13..f6be3387 100644 --- a/ui/src/stores/sideNav.ts +++ b/ui/src/stores/sideNav.ts @@ -10,7 +10,7 @@ interface ErrorCodeType { can_revision: boolean; revision: number; updateVisible: () => void; - updateReiview: (params: reviewData) => void; + updateReview: (params: reviewData) => void; } const Index = create((set) => ({ @@ -22,7 +22,7 @@ const Index = create((set) => ({ return { visible: !state.visible }; }); }, - updateReiview: (params: reviewData) => { + updateReview: (params: reviewData) => { set(() => { return { ...params }; }); diff --git a/ui/src/utils/color.ts b/ui/src/utils/color.ts index 2864d191..bcc58732 100644 --- a/ui/src/utils/color.ts +++ b/ui/src/utils/color.ts @@ -1,5 +1,25 @@ import Color from 'color'; +/** + * Bootstrap Color Weight: + * $blue-100: tint-color($blue, 80%) !default; + * $blue-200: tint-color($blue, 60%) !default; + * $blue-300: tint-color($blue, 40%) !default; + * $blue-400: tint-color($blue, 20%) !default; + * $blue-500: $blue !default; + * $blue-600: shade-color($blue, 20%) !default; + * $blue-700: shade-color($blue, 40%) !default; + * $blue-800: shade-color($blue, 60%) !default; + * $blue-900: shade-color($blue, 80%) !default; + */ + +/** + * The `weight` parameter in `Color`: + * 1. Must use decimals rather than percentages. eg: color.mix(Color("blue"), 0.6) + * 2. The value is the difference between `1 - $weight` in `bootstrap`. + * eg: color.mix(Color("blue"), 0.6) === shade-color($blue, 40%) !default + */ + const WHITE = Color('#fff'); const BLACK = Color('#000'); diff --git a/ui/src/utils/common.ts b/ui/src/utils/common.ts index 7814a966..efd93e0a 100644 --- a/ui/src/utils/common.ts +++ b/ui/src/utils/common.ts @@ -2,8 +2,7 @@ import i18next from 'i18next'; import pattern from '@/common/pattern'; import { USER_AGENT_NAMES } from '@/common/constants'; - -const Diff = require('diff'); +import type * as Type from '@/common/interface'; function thousandthDivision(num) { const reg = /\d{1,3}(?=(\d{3})+$)/g; @@ -112,64 +111,9 @@ function escapeRemove(str: string) { temp = null; return output; } -function mixColor(color_1, color_2, weight) { - function d2h(d) { - return d.toString(16); - } - function h2d(h) { - return parseInt(h, 16); - } - - weight = typeof weight !== 'undefined' ? weight : 50; - let color = '#'; - - for (let i = 0; i <= 5; i += 2) { - const v1 = h2d(color_1.substr(i, 2)); - const v2 = h2d(color_2.substr(i, 2)); - let val = d2h(Math.floor(v2 + (v1 - v2) * (weight / 100.0))); - - while (val.length < 2) { - val = `0${val}`; - } - - color += val; - } - - return color; -} - -function colorRgb(sColor) { - sColor = sColor.toLowerCase(); - const reg = /^#([0-9a-fA-f]{3}|[0-9a-fA-f]{6})$/; - if (sColor && reg.test(sColor)) { - if (sColor.length === 4) { - let sColorNew = '#'; - for (let i = 1; i < 4; i += 1) { - sColorNew += sColor.slice(i, i + 1).concat(sColor.slice(i, i + 1)); - } - sColor = sColorNew; - } - const sColorChange: number[] = []; - for (let i = 1; i < 7; i += 2) { - sColorChange.push(parseInt(`0x${sColor.slice(i, i + 2)}`, 16)); - } - return sColorChange.join(','); - } - return sColor; -} - -function labelStyle(color, hover) { - const textColor = mixColor('000000', color.replace('#', ''), 40); - const backgroundColor = mixColor('ffffff', color.replace('#', ''), 80); - const rgbBackgroundColor = colorRgb(backgroundColor); - return { - color: textColor, - backgroundColor: `rgba(${colorRgb(rgbBackgroundColor)},${hover ? 1 : 0.5})`, - }; -} function handleFormError( - error: { list: Array<{ error_field: string; error_msg: string }> }, + error: { list: Type.FieldError[] }, data: any, keymap?: Array<{ from: string; to: string }>, ) { @@ -203,6 +147,8 @@ function escapeHtml(str: string) { return str.replace(/[&<>"'`]/g, (tag) => tagsToReplace[tag] || tag); } +const Diff = require('diff'); + function diffText(newText: string, oldText?: string): string { if (!newText) { return ''; @@ -285,9 +231,6 @@ export { parseUserInfo, formatUptime, escapeRemove, - mixColor, - colorRgb, - labelStyle, handleFormError, diffText, base64ToSvg, diff --git a/ui/src/utils/request.ts b/ui/src/utils/request.ts index e8cd3b6e..7132099d 100644 --- a/ui/src/utils/request.ts +++ b/ui/src/utils/request.ts @@ -61,6 +61,7 @@ class Request { config: errConfig, } = error.response || {}; const { data = {}, msg = '' } = errBody || {}; + const errorObject: { code: any; msg: string; @@ -74,6 +75,7 @@ class Request { msg, data, }; + if (status === 400) { if (data?.err_type && errConfig?.passingError) { return Promise.reject(errorObject); @@ -127,6 +129,7 @@ class Request { floppyNavigation.navigateToLogin(); return Promise.reject(false); } + if (status === 403) { // Permission interception if (data?.type === 'url_expired') { @@ -173,6 +176,7 @@ class Request { errorCodeStore.getState().update('404'); return Promise.reject(false); } + if (status >= 500) { if (isIgnoredPath(IGNORE_PATH_LIST)) { return Promise.reject(false);