diff --git a/cmd/wire_gen.go b/cmd/wire_gen.go index 1c237435..429771a2 100644 --- a/cmd/wire_gen.go +++ b/cmd/wire_gen.go @@ -45,7 +45,7 @@ import ( "github.com/answerdev/answer/internal/service" "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 +56,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 +129,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,10 +139,12 @@ 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) @@ -150,11 +152,11 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database, 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) + serviceVoteRepo := activity.NewVoteRepo(dataData, activityRepo, userRankRepo, notificationQueueService) + voteService := service.NewVoteService(serviceVoteRepo, configService, questionRepo, answerRepo, commentCommonRepo, objService) voteController := controller.NewVoteController(voteService, rankService) 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,25 +167,23 @@ 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) + answerController := controller.NewAnswerController(answerService, rankService) 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) + 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) @@ -195,36 +195,38 @@ 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) commentCommonService := comment_common.NewCommentCommonService(commentCommonRepo) - activityService := activity2.NewActivityService(activityActivityRepo, userCommon, activityCommon, tagCommonService, objService, commentCommonService, revisionService, metaService, configService) - activityController := controller.NewActivityController(activityCommon, activityService) + activityService := activity2.NewActivityService(activityActivityRepo, userCommon, tagCommonService, objService, commentCommonService, revisionService, metaService, configService) + 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) 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..780e41dd 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -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", @@ -8186,7 +8186,7 @@ const docTemplate = `{ "type": "string" }, "site_seo": { - "$ref": "#/definitions/schema.SiteSeoReq" + "$ref": "#/definitions/schema.SiteSeoResp" }, "site_users": { "$ref": "#/definitions/schema.SiteUsersResp" diff --git a/docs/swagger.json b/docs/swagger.json index 8a0db0e1..df3eee70 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", @@ -8174,7 +8174,7 @@ "type": "string" }, "site_seo": { - "$ref": "#/definitions/schema.SiteSeoReq" + "$ref": "#/definitions/schema.SiteSeoResp" }, "site_users": { "$ref": "#/definitions/schema.SiteUsersResp" diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 4f73fe51..e2b9c829 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1507,7 +1507,7 @@ definitions: revision: type: string site_seo: - $ref: '#/definitions/schema.SiteSeoReq' + $ref: '#/definitions/schema.SiteSeoResp' site_users: $ref: '#/definitions/schema.SiteUsersResp' theme: @@ -2335,7 +2335,7 @@ paths: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] - summary: AdminSearchAnswerList + summary: AdminAnswerPage admin answer page tags: - admin /answer/admin/api/answer/status: @@ -2533,7 +2533,7 @@ paths: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] - summary: AdminSearchList + summary: AdminQuestionPage admin question page tags: - admin /answer/admin/api/question/status: diff --git a/go.mod b/go.mod index 1e861790..e7c08a67 100644 --- a/go.mod +++ b/go.mod @@ -48,7 +48,6 @@ require ( gopkg.in/yaml.v3 v3.0.1 modernc.org/sqlite v1.14.2 xorm.io/builder v0.3.12 - xorm.io/core v0.7.3 xorm.io/xorm v1.3.2 ) diff --git a/go.sum b/go.sum index e26ab91c..7ece6c5c 100644 --- a/go.sum +++ b/go.sum @@ -221,7 +221,6 @@ github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXS 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-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= @@ -493,7 +492,6 @@ github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27k 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-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= @@ -1084,7 +1082,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= @@ -1329,7 +1326,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 7ca9c6fa..ebbc9e35 100644 --- a/i18n/en_US.yaml +++ b/i18n/en_US.yaml @@ -1417,6 +1417,7 @@ ui: change: Change all: All staff: Staff + more: More inactive: Inactive suspended: Suspended deleted: Deleted 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..64e59dc1 100644 --- a/internal/base/middleware/auth.go +++ b/internal/base/middleware/auth.go @@ -6,13 +6,13 @@ import ( "github.com/answerdev/answer/internal/schema" "github.com/answerdev/answer/internal/service/role" "github.com/answerdev/answer/internal/service/siteinfo_common" + "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" ) @@ -21,13 +21,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, 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/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..bf343616 100644 --- a/internal/controller/answer_controller.go +++ b/internal/controller/answer_controller.go @@ -8,7 +8,6 @@ import ( "github.com/answerdev/answer/internal/base/reason" "github.com/answerdev/answer/internal/schema" "github.com/answerdev/answer/internal/service" - "github.com/answerdev/answer/internal/service/dashboard" "github.com/answerdev/answer/internal/service/permission" "github.com/answerdev/answer/internal/service/rank" "github.com/answerdev/answer/pkg/uid" @@ -18,20 +17,17 @@ import ( // AnswerController answer controller type AnswerController struct { - answerService *service.AnswerService - rankService *rank.RankService - dashboardService *dashboard.DashboardService + answerService *service.AnswerService + rankService *rank.RankService } // NewAnswerController new controller func NewAnswerController(answerService *service.AnswerService, rankService *rank.RankService, - dashboardService *dashboard.DashboardService, ) *AnswerController { return &AnswerController{ - answerService: answerService, - rankService: rankService, - dashboardService: dashboardService, + answerService: answerService, + rankService: rankService, } } diff --git a/internal/controller/connector_controller.go b/internal/controller/connector_controller.go index f3bfdc5a..f6c35d70 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 { 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..aa24fd32 100644 --- a/internal/controller/question_controller.go +++ b/internal/controller/question_controller.go @@ -7,11 +7,11 @@ 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/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 +23,7 @@ type QuestionController struct { questionService *service.QuestionService answerService *service.AnswerService rankService *rank.RankService + siteInfoService siteinfo_common.SiteInfoCommonService } // NewQuestionController new controller @@ -30,11 +31,13 @@ func NewQuestionController( questionService *service.QuestionService, answerService *service.AnswerService, rankService *rank.RankService, + siteInfoService siteinfo_common.SiteInfoCommonService, ) *QuestionController { return &QuestionController{ questionService: questionService, answerService: answerService, rankService: rankService, + siteInfoService: siteInfoService, } } @@ -220,7 +223,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) } @@ -703,8 +708,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 +721,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 +746,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/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 cf9dd5ee..a291b7d8 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) } } @@ -186,11 +191,11 @@ func (tc *TemplateController) QuestionInfoeRdirect(ctx *gin.Context, siteInfo *s } 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 +221,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, "" @@ -277,9 +282,11 @@ func (tc *TemplateController) QuestionInfo(ctx *gin.Context) { } // comments - objectIDs := []string{id} + + objectIDs := []string{uid.DeShortID(id)} for _, answer := range answers { - objectIDs = append(objectIDs, answer.ID) + answerID := uid.DeShortID(answer.ID) + objectIDs = append(objectIDs, answerID) } comments, err := tc.templateRenderController.CommentList(ctx, objectIDs) if err != nil { @@ -287,7 +294,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{} @@ -402,7 +409,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..ee216481 100644 --- a/internal/controller/user_controller.go +++ b/internal/controller/user_controller.go @@ -12,7 +12,6 @@ import ( "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 +23,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 +33,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, } diff --git a/internal/controller/vote_controller.go b/internal/controller/vote_controller.go index 6af80e35..3548e053 100644 --- a/internal/controller/vote_controller.go +++ b/internal/controller/vote_controller.go @@ -10,7 +10,6 @@ import ( "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" ) @@ -54,9 +53,7 @@ func (vc *VoteController) VoteUp(ctx *gin.Context) { return } - dto := &schema.VoteDTO{} - _ = copier.Copy(dto, req) - resp, err := vc.VoteService.VoteUp(ctx, dto) + resp, err := vc.VoteService.VoteUp(ctx, req) if err != nil { handler.HandleResponse(ctx, err, schema.ErrTypeToast) } else { @@ -93,9 +90,7 @@ func (vc *VoteController) VoteDown(ctx *gin.Context) { return } - dto := &schema.VoteDTO{} - _ = copier.Copy(dto, req) - resp, err := vc.VoteService.VoteDown(ctx, dto) + resp, err := vc.VoteService.VoteDown(ctx, req) if err != nil { handler.HandleResponse(ctx, err, schema.ErrTypeToast) } else { @@ -103,9 +98,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 +111,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/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/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..19b03925 100644 --- a/internal/migrations/init.go +++ b/internal/migrations/init.go @@ -1,188 +1,183 @@ package migrations import ( + "context" "encoding/json" "fmt" - "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 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) + 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) 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 +186,9 @@ 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 - } - adminPassword = string(generateFromPassword) - - // update admin info - _, err = engine.ID("1").Update(&entity.User{ - Username: adminName, - Pass: adminPassword, - EMail: adminEmail, - DisplayName: adminName, - }) - 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..623fa5d2 --- /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: `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`}, + } +) diff --git a/internal/repo/activity/answer_repo.go b/internal/repo/activity/answer_repo.go index 6ed8bad4..349fbb7a 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,338 @@ 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)) - + // 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 { + if _, err = session.Where("id = ?", existsActivity.ID).Cols("`cancelled`"). + Update(&entity.Activity{Cancelled: entity.ActivityAvailable}); 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..120d392b 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, 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,243 @@ 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, maxDailyRank int) (err error) { + // check if user reach daily rank limit + for _, activity := range op.Activities { + reach, err := vr.userRankRepo.CheckReachLimit(ctx, session, activity.ActivityUserID, maxDailyRank) + if err != nil { + log.Error(err) + return err + } + if reach { + activity.Rank = 0 + } + } + 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 { + if _, err = session.Where("id = ?", existsActivity.ID).Cols("`cancelled`"). + Update(&entity.Activity{Cancelled: entity.ActivityAvailable}); 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 +427,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 +463,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..2cf18c9d 100644 --- a/internal/repo/answer/answer_repo.go +++ b/internal/repo/answer/answer_repo.go @@ -2,14 +2,11 @@ package answer import ( "context" - "strings" "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 +55,10 @@ 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) + } return nil } @@ -109,9 +108,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 +134,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 +152,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 } @@ -191,8 +195,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 +228,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 +282,40 @@ 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 } diff --git a/internal/repo/config/config_repo.go b/internal/repo/config/config_repo.go index c5a155bd..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() } @@ -80,7 +80,7 @@ func (cr configRepo) GetConfigByKey(ctx context.Context, key string) (c *entity. func (cr configRepo) UpdateConfig(ctx context.Context, key string, value string) (err error) { // check if key exists - oldConfig := &entity.Config{} + oldConfig := &entity.Config{Key: key} exist, err := cr.data.DB.Context(ctx).Get(oldConfig) if err != nil { return 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..6623ae90 100644 --- a/internal/repo/question/question_repo.go +++ b/internal/repo/question/question_repo.go @@ -2,11 +2,14 @@ package question import ( "context" + "encoding/json" "fmt" + "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 +53,9 @@ 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) + } return } @@ -71,7 +76,9 @@ 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) + } return } @@ -164,7 +171,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 +184,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 +201,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,13 +224,13 @@ 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.Where("status = ? OR status = ?", 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) { @@ -238,38 +251,52 @@ func (qr *questionRepo) GetQuestionCountByIDs(ctx context.Context, ids []string) 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 +339,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 +408,10 @@ 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 } diff --git a/internal/repo/rank/user_rank_repo.go b/internal/repo/rank/user_rank_repo.go index 06570fec..257dec8e 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.Sum(&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/tag/tag_rel_repo.go b/internal/repo/tag/tag_rel_repo.go index 62b3fe7a..3570cc01 100644 --- a/internal/repo/tag/tag_rel_repo.go +++ b/internal/repo/tag/tag_rel_repo.go @@ -4,6 +4,7 @@ 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 +37,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 +57,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 +66,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 +92,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 +118,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 +139,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..9bf2f2ea 100644 --- a/internal/repo/user/user_repo.go +++ b/internal/repo/user/user_repo.go @@ -72,7 +72,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 +82,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 +195,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 diff --git a/internal/router/answer_api_router.go b/internal/router/answer_api_router.go index 025ee594..897deace 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, @@ -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 diff --git a/internal/router/template_router.go b/internal/router/template_router.go index 4ecb99aa..e90dc7ee 100644 --- a/internal/router/template_router.go +++ b/internal/router/template_router.go @@ -17,7 +17,6 @@ func NewTemplateRouter( templateController *controller.TemplateController, templateRenderController *templaterender.TemplateRenderController, siteInfoController *controller_admin.SiteInfoController, - ) *TemplateRouter { return &TemplateRouter{ templateController: templateController, @@ -26,7 +25,7 @@ func NewTemplateRouter( } } -// 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) @@ -35,7 +34,6 @@ func (a *TemplateRouter) RegisterTemplateRouter(r *gin.RouterGroup) { 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) 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..747f7754 --- /dev/null +++ b/internal/schema/answer_activity_schema.go @@ -0,0 +1,35 @@ +package schema + +// AcceptAnswerOperationInfo accept answer operation info +type AcceptAnswerOperationInfo struct { + 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/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..6fd26bab 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" @@ -361,12 +361,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 +424,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/siteinfo_schema.go b/internal/schema/siteinfo_schema.go index 119420cf..bef9cf8e 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 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/vote_schema.go b/internal/schema/vote_schema.go index 4cb86c66..e41f542f 100644 --- a/internal/schema/vote_schema.go +++ b/internal/schema/vote_schema.go @@ -6,20 +6,44 @@ type VoteReq struct { UserID string `json:"-"` } -type VoteDTO struct { - // object TagID - ObjectID string - // is cancel - IsCancel bool - // user TagID - UserID string +type VoteResp struct { + UpVotes int64 `json:"up_votes"` + DownVotes int64 `json:"down_votes"` + Votes int64 `json:"votes"` + VoteStatus string `json:"vote_status"` } -type VoteResp struct { - UpVotes int `json:"up_votes"` - DownVotes int `json:"down_votes"` - Votes int `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 { @@ -28,23 +52,7 @@ type GetVoteWithPageReq struct { // 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/activity/activity.go b/internal/service/activity/activity.go index ebd5e6df..0b3f7dde 100644 --- a/internal/service/activity/activity.go +++ b/internal/service/activity/activity.go @@ -7,9 +7,9 @@ import ( "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" @@ -30,22 +30,20 @@ type ActivityRepo interface { // ActivityService activity service type ActivityService struct { - activityRepo ActivityRepo - userCommon *usercommon.UserCommon - activityCommonService *activity_common.ActivityCommon - tagCommonService *tag_common.TagCommonService - objectInfoService *object_info.ObjService - commentCommonService *comment_common.CommentCommonService - revisionService *revision_common.RevisionService - metaService *meta.MetaService - configService *config.ConfigService + activityRepo ActivityRepo + userCommon *usercommon.UserCommon + tagCommonService *tag_common.TagCommonService + objectInfoService *object_info.ObjService + commentCommonService *comment_common.CommentCommonService + revisionService *revision_common.RevisionService + metaService *meta.MetaService + configService *config.ConfigService } // NewActivityService new activity service func NewActivityService( activityRepo ActivityRepo, userCommon *usercommon.UserCommon, - activityCommonService *activity_common.ActivityCommon, tagCommonService *tag_common.TagCommonService, objectInfoService *object_info.ObjService, commentCommonService *comment_common.CommentCommonService, @@ -54,15 +52,14 @@ func NewActivityService( configService *config.ConfigService, ) *ActivityService { return &ActivityService{ - objectInfoService: objectInfoService, - activityRepo: activityRepo, - userCommon: userCommon, - activityCommonService: activityCommonService, - tagCommonService: tagCommonService, - commentCommonService: commentCommonService, - revisionService: revisionService, - metaService: metaService, - configService: configService, + objectInfoService: objectInfoService, + activityRepo: activityRepo, + userCommon: userCommon, + tagCommonService: tagCommonService, + commentCommonService: commentCommonService, + revisionService: revisionService, + metaService: metaService, + configService: configService, } } @@ -97,7 +94,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..56b09676 --- /dev/null +++ b/internal/service/activity/answer_activity_service.go @@ -0,0 +1,92 @@ +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, + answerObjID, questionObjID, questionUserID, answerUserID string, isSelf bool) (err error) { + operationInfo := as.createAcceptAnswerOperationInfo(ctx, + answerObjID, questionObjID, questionUserID, answerUserID, isSelf) + return as.answerActivityRepo.SaveAcceptAnswerActivity(ctx, operationInfo) +} + +// CancelAcceptAnswer cancel accept answer change activity +func (as *AnswerActivityService) CancelAcceptAnswer(ctx context.Context, + answerObjID, questionObjID, questionUserID, answerUserID string) (err error) { + operationInfo := as.createAcceptAnswerOperationInfo(ctx, + answerObjID, questionObjID, questionUserID, answerUserID, false) + return as.answerActivityRepo.SaveCancelAcceptAnswerActivity(ctx, operationInfo) +} + +func (as *AnswerActivityService) createAcceptAnswerOperationInfo(ctx context.Context, + answerObjID, questionObjID, questionUserID, answerUserID string, isSelf bool) *schema.AcceptAnswerOperationInfo { + operationInfo := &schema.AcceptAnswerOperationInfo{ + 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.AnswerUserID + t.OriginalObjectID = op.QuestionObjectID // if activity is 'accept' means this question is accept the answer. + } else { + t.ActivityUserID = op.AnswerUserID + t.TriggerUserID = op.AnswerUserID + 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..81276951 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, @@ -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/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..c886faee 100644 --- a/internal/service/export/email_service.go +++ b/internal/service/export/email_service.go @@ -106,6 +106,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/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..dee28b5b 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,27 @@ 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 +80,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 +514,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 +552,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 +581,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..f8f939b7 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, @@ -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/siteinfo/siteinfo_service.go b/internal/service/siteinfo/siteinfo_service.go index 5d0fc294..70ddadba 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) { 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..ad6ea194 100644 --- a/internal/service/user_admin/user_backyard.go +++ b/internal/service/user_admin/user_backyard.go @@ -41,7 +41,7 @@ type UserAdminService struct { authService *auth.AuthService userCommonService *usercommon.UserCommon userActivity activity.UserActiveActivityRepo - siteInfoCommonService *siteinfo_common.SiteInfoCommonService + siteInfoCommonService siteinfo_common.SiteInfoCommonService } // NewUserAdminService new user admin service @@ -51,7 +51,7 @@ func NewUserAdminService( authService *auth.AuthService, userCommonService *usercommon.UserCommon, userActivity activity.UserActiveActivityRepo, - siteInfoCommonService *siteinfo_common.SiteInfoCommonService, + siteInfoCommonService siteinfo_common.SiteInfoCommonService, ) *UserAdminService { return &UserAdminService{ userRepo: userRepo, diff --git a/internal/service/user_common/user.go b/internal/service/user_common/user.go index 9a451265..84a13f3e 100644 --- a/internal/service/user_common/user.go +++ b/internal/service/user_common/user.go @@ -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, 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 9e870381..e4f786d6 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, @@ -228,10 +228,10 @@ func (us *UserCenterLoginService) UserCenterAdminFunctionAgent(ctx context.Conte desc := userCenter.Description() // If user status agent is enabled, admin can not update user status in answer. resp.AllowUpdateUserStatus = !desc.UserStatusAgentEnabled + resp.AllowUpdateUserRole = !desc.UserRoleAgentEnabled // If original user system is enabled, admin can update user password and role in answer. resp.AllowUpdateUserPassword = desc.EnabledOriginalUserSystem - resp.AllowUpdateUserRole = desc.EnabledOriginalUserSystem resp.AllowCreateUser = desc.EnabledOriginalUserSystem return resp, 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..582a4cb1 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 } diff --git a/internal/service/vote_service.go b/internal/service/vote_service.go index 022f9dca..61f7e31d 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,87 @@ 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) } + + 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 +162,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 +192,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/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..3c8ab899 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) diff --git a/plugin/user_center.go b/plugin/user_center.go index 57061394..ee6d7431 100644 --- a/plugin/user_center.go +++ b/plugin/user_center.go @@ -33,6 +33,7 @@ type UserCenterDesc struct { SignUpRedirectURL string `json:"sign_up_redirect_url"` RankAgentEnabled bool `json:"rank_agent_enabled"` UserStatusAgentEnabled bool `json:"user_status_agent_enabled"` + UserRoleAgentEnabled bool `json:"user_role_agent_enabled"` MustAuthEmailEnabled bool `json:"must_auth_email_enabled"` EnabledOriginalUserSystem bool `json:"enabled_original_user_system"` } diff --git a/ui/src/plugins/builtin/UcLogin/i18n/en_US.yaml b/ui/src/plugins/builtin/UcLogin/i18n/en_US.yaml index b61e3d64..a141b60a 100644 --- a/ui/src/plugins/builtin/UcLogin/i18n/en_US.yaml +++ b/ui/src/plugins/builtin/UcLogin/i18n/en_US.yaml @@ -1,6 +1,7 @@ plugin: uc_login: ui: + connect: Connect with {{ auth_name }} login: Login qrcode_login_tip: Please use {{ agentName }} to scan the QR code and log in. login_failed_email_tip: Login failed, please allow this app to access your email information before try again. diff --git a/ui/src/plugins/builtin/UcLogin/i18n/zh_CN.yaml b/ui/src/plugins/builtin/UcLogin/i18n/zh_CN.yaml index d8956ef3..492041a8 100644 --- a/ui/src/plugins/builtin/UcLogin/i18n/zh_CN.yaml +++ b/ui/src/plugins/builtin/UcLogin/i18n/zh_CN.yaml @@ -1,6 +1,7 @@ plugin: uc_login: ui: + connect: 连接到 {{ auth_name }} login: 登录 qrcode_login_tip: 请使用 {{ agentName }} 扫描二维码登录 login_failed_email_tip: 登录失败, 请允许该应用程序访问您的电子邮件信息,然后再试一次。