diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index c80bde9f..cc4dd08e 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -27,6 +27,8 @@ stages: "compile the golang project": image: golang:1.18 stage: compile-golang + before_script: + - export GOPROXY=https://goproxy.cn,direct script: - make generate - make build @@ -36,6 +38,8 @@ stages: "build docker images and push": stage: push + before_script: + - export GOPROXY=https://goproxy.cn,direct extends: .docker-build-push only: - test diff --git a/cmd/answer/wire_gen.go b/cmd/answer/wire_gen.go index cc67b19e..a48ecaf2 100644 --- a/cmd/answer/wire_gen.go +++ b/cmd/answer/wire_gen.go @@ -31,7 +31,6 @@ import ( "github.com/answerdev/answer/internal/repo/reason" "github.com/answerdev/answer/internal/repo/report" "github.com/answerdev/answer/internal/repo/revision" - "github.com/answerdev/answer/internal/repo/role" "github.com/answerdev/answer/internal/repo/search_common" "github.com/answerdev/answer/internal/repo/site_info" "github.com/answerdev/answer/internal/repo/tag" @@ -42,10 +41,12 @@ 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/answer_common" auth2 "github.com/answerdev/answer/internal/service/auth" "github.com/answerdev/answer/internal/service/collection_common" comment2 "github.com/answerdev/answer/internal/service/comment" + "github.com/answerdev/answer/internal/service/comment_common" "github.com/answerdev/answer/internal/service/dashboard" export2 "github.com/answerdev/answer/internal/service/export" "github.com/answerdev/answer/internal/service/follow" @@ -60,7 +61,6 @@ import ( "github.com/answerdev/answer/internal/service/report_backyard" "github.com/answerdev/answer/internal/service/report_handle_backyard" "github.com/answerdev/answer/internal/service/revision_common" - role2 "github.com/answerdev/answer/internal/service/role" "github.com/answerdev/answer/internal/service/search_parser" "github.com/answerdev/answer/internal/service/service_config" "github.com/answerdev/answer/internal/service/siteinfo" @@ -120,7 +120,12 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database, answerRepo := answer.NewAnswerRepo(dataData, uniqueIDRepo, userRankRepo, activityRepo) questionRepo := question.NewQuestionRepo(dataData, uniqueIDRepo) tagCommonRepo := tag_common.NewTagCommonRepo(dataData, uniqueIDRepo) - objService := object_info.NewObjService(answerRepo, questionRepo, commentCommonRepo, tagCommonRepo) + tagRelRepo := tag.NewTagRelRepo(dataData) + 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) + objService := object_info.NewObjService(answerRepo, questionRepo, commentCommonRepo, tagCommonRepo, tagCommonService) voteRepo := activity_common.NewVoteRepo(dataData, activityRepo) commentService := comment2.NewCommentService(commentRepo, commentCommonRepo, userCommon, objService, voteRepo) rankService := rank2.NewRankService(userCommon, userRankRepo, objService, configRepo) @@ -130,12 +135,7 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database, reportController := controller.NewReportController(reportService, rankService) serviceVoteRepo := activity.NewVoteRepo(dataData, uniqueIDRepo, configRepo, activityRepo, userRankRepo, voteRepo) voteService := service.NewVoteService(serviceVoteRepo, uniqueIDRepo, configRepo, questionRepo, answerRepo, commentCommonRepo, objService) - voteController := controller.NewVoteController(voteService) - tagRepo := tag.NewTagRepo(dataData, uniqueIDRepo) - tagRelRepo := tag.NewTagRelRepo(dataData) - revisionRepo := revision.NewRevisionRepo(dataData, uniqueIDRepo) - revisionService := revision_common.NewRevisionService(revisionRepo, userRepo) - tagCommonService := tag_common2.NewTagCommonService(tagCommonRepo, tagRelRepo, revisionService, siteInfoCommonService) + voteController := controller.NewVoteController(voteService, rankService) followRepo := activity_common.NewFollowRepo(dataData, uniqueIDRepo, activityRepo) tagService := tag2.NewTagService(tagRepo, tagCommonService, revisionService, followRepo, siteInfoCommonService) tagController := controller.NewTagController(tagService, tagCommonService, rankService) @@ -163,19 +163,15 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database, searchRepo := search_common.NewSearchRepo(dataData, uniqueIDRepo, userCommon) searchService := service.NewSearchService(searchParser, searchRepo) searchController := controller.NewSearchController(searchService) - serviceRevisionService := service.NewRevisionService(revisionRepo, userCommon, questionCommon, answerService) - revisionController := controller.NewRevisionController(serviceRevisionService) + serviceRevisionService := service.NewRevisionService(revisionRepo, userCommon, questionCommon, answerService, objService, questionRepo, answerRepo, tagRepo, tagCommonService) + revisionController := controller.NewRevisionController(serviceRevisionService, rankService) rankController := controller.NewRankController(rankService) commonRepo := common.NewCommonRepo(dataData, uniqueIDRepo) reportHandle := report_handle_backyard.NewReportHandle(questionCommon, commentRepo, configRepo) reportBackyardService := report_backyard.NewReportBackyardService(reportRepo, userCommon, commonRepo, answerRepo, questionRepo, commentCommonRepo, reportHandle, configRepo) controller_backyardReportController := controller_backyard.NewReportController(reportBackyardService) userBackyardRepo := user.NewUserBackyardRepo(dataData, authRepo) - userRoleRelRepo := role.NewUserRoleRelRepo(dataData) - roleRepo := role.NewRoleRepo(dataData) - roleService := role2.NewRoleService(roleRepo) - userRoleRelService := role2.NewUserRoleRelService(userRoleRelRepo, roleService) - userBackyardService := user_backyard.NewUserBackyardService(userBackyardRepo, userRoleRelService) + userBackyardService := user_backyard.NewUserBackyardService(userBackyardRepo) userBackyardController := controller_backyard.NewUserBackyardController(userBackyardService) reasonRepo := reason.NewReasonRepo(configRepo) reasonService := reason2.NewReasonService(reasonRepo) @@ -186,12 +182,16 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database, siteinfoController := controller.NewSiteinfoController(siteInfoCommonService) notificationRepo := notification.NewNotificationRepo(dataData) notificationCommon := notificationcommon.NewNotificationCommon(dataData, notificationRepo, userCommon, activityRepo, followRepo, objService) - notificationService := notification2.NewNotificationService(dataData, notificationRepo, notificationCommon) - notificationController := controller.NewNotificationController(notificationService) + notificationService := notification2.NewNotificationService(dataData, notificationRepo, notificationCommon, revisionService) + notificationController := controller.NewNotificationController(notificationService, rankService) dashboardController := controller.NewDashboardController(dashboardService) uploadController := controller.NewUploadController(uploaderService) - roleController := controller_backyard.NewRoleController(roleService) - answerAPIRouter := router.NewAnswerAPIRouter(langController, userController, commentController, reportController, voteController, tagController, followController, collectionController, questionController, answerController, searchController, revisionController, rankController, controller_backyardReportController, userBackyardController, reasonController, themeController, siteInfoController, siteinfoController, notificationController, dashboardController, uploadController, roleController) + activityCommon := activity_common2.NewActivityCommon(activityRepo) + activityActivityRepo := activity.NewActivityRepo(dataData) + commentCommonService := comment_common.NewCommentCommonService(commentCommonRepo) + activityService := activity2.NewActivityService(activityActivityRepo, userCommon, activityCommon, tagCommonService, objService, commentCommonService, revisionService) + activityController := controller.NewActivityController(activityCommon, activityService) + answerAPIRouter := router.NewAnswerAPIRouter(langController, userController, commentController, reportController, voteController, tagController, followController, collectionController, questionController, answerController, searchController, revisionController, rankController, controller_backyardReportController, userBackyardController, reasonController, themeController, siteInfoController, siteinfoController, notificationController, dashboardController, uploadController, activityController) swaggerRouter := router.NewSwaggerRouter(swaggerConf) uiRouter := router.NewUIRouter() authUserMiddleware := middleware.NewAuthUserMiddleware(authService) diff --git a/docs/docs.go b/docs/docs.go index c3bb65f4..aa733960 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -1085,6 +1085,110 @@ const docTemplate = `{ } } }, + "/answer/api/v1/activity/timeline": { + "get": { + "description": "get object timeline", + "produces": [ + "application/json" + ], + "tags": [ + "Comment" + ], + "summary": "get object timeline", + "parameters": [ + { + "type": "string", + "description": "object id", + "name": "object_id", + "in": "query" + }, + { + "type": "string", + "description": "tag slug name", + "name": "tag_slug_name", + "in": "query" + }, + { + "enum": [ + "question", + "answer", + "tag" + ], + "type": "string", + "description": "object type", + "name": "object_type", + "in": "query" + }, + { + "type": "boolean", + "description": "is show vote", + "name": "show_vote", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/schema.GetObjectTimelineResp" + } + } + } + ] + } + } + } + } + }, + "/answer/api/v1/activity/timeline/detail": { + "get": { + "description": "get object timeline detail", + "produces": [ + "application/json" + ], + "tags": [ + "Comment" + ], + "summary": "get object timeline detail", + "parameters": [ + { + "type": "string", + "description": "revision id", + "name": "revision_id", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/schema.GetObjectTimelineResp" + } + } + } + ] + } + } + } + } + }, "/answer/api/v1/answer": { "put": { "security": [ @@ -1290,12 +1394,12 @@ const docTemplate = `{ "summary": "AnswerList", "parameters": [ { - "description": "AnswerList", + "description": "AnswerListReq", "name": "data", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/schema.AnswerList" + "$ref": "#/definitions/schema.AnswerListReq" } } ], @@ -3007,6 +3111,141 @@ const docTemplate = `{ } } }, + "/answer/api/v1/revisions/audit": { + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "revision audit operation:approve or reject", + "produces": [ + "application/json" + ], + "tags": [ + "Revision" + ], + "summary": "revision audit", + "parameters": [ + { + "description": "audit", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.RevisionAuditReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, + "/answer/api/v1/revisions/edit/check": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "check can update revision", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Revision" + ], + "summary": "check can update revision", + "parameters": [ + { + "type": "string", + "default": "string", + "description": "id", + "name": "id", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, + "/answer/api/v1/revisions/unreviewed": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "get unreviewed revision list", + "produces": [ + "application/json" + ], + "tags": [ + "Revision" + ], + "summary": "get unreviewed revision list", + "parameters": [ + { + "type": "string", + "description": "page id", + "name": "page", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "allOf": [ + { + "$ref": "#/definitions/pager.PageModel" + }, + { + "type": "object", + "properties": { + "list": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.GetUnreviewedRevisionResp" + } + } + } + } + ] + } + } + } + ] + } + } + } + } + }, "/answer/api/v1/search": { "get": { "security": [ @@ -3323,10 +3562,7 @@ const docTemplate = `{ "type": "object", "properties": { "data": { - "type": "array", - "items": { - "$ref": "#/definitions/schema.GetTagSynonymsResp" - } + "$ref": "#/definitions/schema.GetTagSynonymsResp" } } } @@ -4548,6 +4784,67 @@ const docTemplate = `{ "list": {} } }, + "schema.ActObjectInfo": { + "type": "object", + "properties": { + "answer_id": { + "type": "string" + }, + "display_name": { + "type": "string" + }, + "object_type": { + "type": "string" + }, + "question_id": { + "type": "string" + }, + "title": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "schema.ActObjectTimeline": { + "type": "object", + "properties": { + "activity_id": { + "type": "string" + }, + "activity_type": { + "type": "string" + }, + "cancelled": { + "type": "boolean" + }, + "cancelled_at": { + "type": "integer" + }, + "comment": { + "type": "string" + }, + "created_at": { + "type": "integer" + }, + "object_id": { + "type": "string" + }, + "object_type": { + "type": "string" + }, + "revision_id": { + "type": "string" + }, + "user_display_name": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, "schema.ActionRecordResp": { "type": "object", "properties": { @@ -4658,7 +4955,7 @@ const docTemplate = `{ } } }, - "schema.AnswerList": { + "schema.AnswerListReq": { "type": "object", "properties": { "order": { @@ -4954,6 +5251,20 @@ const docTemplate = `{ } } }, + "schema.GetObjectTimelineResp": { + "type": "object", + "properties": { + "object_info": { + "$ref": "#/definitions/schema.ActObjectInfo" + }, + "timeline": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.ActObjectTimeline" + } + } + } + }, "schema.GetOtherUserInfoByUsernameResp": { "type": "object", "properties": { @@ -5342,21 +5653,33 @@ const docTemplate = `{ "schema.GetTagSynonymsResp": { "type": "object", "properties": { - "display_name": { - "description": "display name", + "member_actions": { + "description": "MemberActions", + "type": "array", + "items": { + "$ref": "#/definitions/schema.PermissionMemberAction" + } + }, + "synonyms": { + "description": "synonyms", + "type": "array", + "items": { + "$ref": "#/definitions/schema.TagSynonym" + } + } + } + }, + "schema.GetUnreviewedRevisionResp": { + "type": "object", + "properties": { + "info": { + "$ref": "#/definitions/schema.UnreviewedRevisionInfoInfo" + }, + "type": { "type": "string" }, - "main_tag_slug_name": { - "description": "if main tag slug name is not empty, this tag is synonymous with the main tag", - "type": "string" - }, - "slug_name": { - "description": "slug name", - "type": "string" - }, - "tag_id": { - "description": "tag id", - "type": "string" + "unreviewed_info": { + "$ref": "#/definitions/schema.GetRevisionResp" } } }, @@ -5860,6 +6183,23 @@ const docTemplate = `{ } } }, + "schema.RevisionAuditReq": { + "type": "object", + "required": [ + "id", + "operation" + ], + "properties": { + "id": { + "description": "object id", + "type": "string" + }, + "operation": { + "description": "approve or reject", + "type": "string" + } + } + }, "schema.SearchListResp": { "type": "object", "properties": { @@ -5989,9 +6329,7 @@ const docTemplate = `{ "type": "object", "required": [ "contact_email", - "description", "name", - "short_description", "site_url" ], "properties": { @@ -6021,9 +6359,7 @@ const docTemplate = `{ "type": "object", "required": [ "contact_email", - "description", "name", - "short_description", "site_url" ], "properties": { @@ -6211,6 +6547,50 @@ const docTemplate = `{ } } }, + "schema.TagSynonym": { + "type": "object", + "properties": { + "display_name": { + "description": "display name", + "type": "string" + }, + "main_tag_slug_name": { + "description": "if main tag slug name is not empty, this tag is synonymous with the main tag", + "type": "string" + }, + "slug_name": { + "description": "slug name", + "type": "string" + }, + "tag_id": { + "description": "tag id", + "type": "string" + } + } + }, + "schema.UnreviewedRevisionInfoInfo": { + "type": "object", + "properties": { + "content": { + "type": "string" + }, + "html": { + "type": "string" + }, + "object_id": { + "type": "string" + }, + "tags": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.TagResp" + } + }, + "title": { + "type": "string" + } + } + }, "schema.UpdateCommentReq": { "type": "object", "required": [ diff --git a/docs/swagger.json b/docs/swagger.json index 4f224ec6..a60a826c 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -1073,6 +1073,110 @@ } } }, + "/answer/api/v1/activity/timeline": { + "get": { + "description": "get object timeline", + "produces": [ + "application/json" + ], + "tags": [ + "Comment" + ], + "summary": "get object timeline", + "parameters": [ + { + "type": "string", + "description": "object id", + "name": "object_id", + "in": "query" + }, + { + "type": "string", + "description": "tag slug name", + "name": "tag_slug_name", + "in": "query" + }, + { + "enum": [ + "question", + "answer", + "tag" + ], + "type": "string", + "description": "object type", + "name": "object_type", + "in": "query" + }, + { + "type": "boolean", + "description": "is show vote", + "name": "show_vote", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/schema.GetObjectTimelineResp" + } + } + } + ] + } + } + } + } + }, + "/answer/api/v1/activity/timeline/detail": { + "get": { + "description": "get object timeline detail", + "produces": [ + "application/json" + ], + "tags": [ + "Comment" + ], + "summary": "get object timeline detail", + "parameters": [ + { + "type": "string", + "description": "revision id", + "name": "revision_id", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/schema.GetObjectTimelineResp" + } + } + } + ] + } + } + } + } + }, "/answer/api/v1/answer": { "put": { "security": [ @@ -1278,12 +1382,12 @@ "summary": "AnswerList", "parameters": [ { - "description": "AnswerList", + "description": "AnswerListReq", "name": "data", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/schema.AnswerList" + "$ref": "#/definitions/schema.AnswerListReq" } } ], @@ -2995,6 +3099,141 @@ } } }, + "/answer/api/v1/revisions/audit": { + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "revision audit operation:approve or reject", + "produces": [ + "application/json" + ], + "tags": [ + "Revision" + ], + "summary": "revision audit", + "parameters": [ + { + "description": "audit", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.RevisionAuditReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, + "/answer/api/v1/revisions/edit/check": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "check can update revision", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Revision" + ], + "summary": "check can update revision", + "parameters": [ + { + "type": "string", + "default": "string", + "description": "id", + "name": "id", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, + "/answer/api/v1/revisions/unreviewed": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "get unreviewed revision list", + "produces": [ + "application/json" + ], + "tags": [ + "Revision" + ], + "summary": "get unreviewed revision list", + "parameters": [ + { + "type": "string", + "description": "page id", + "name": "page", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "allOf": [ + { + "$ref": "#/definitions/pager.PageModel" + }, + { + "type": "object", + "properties": { + "list": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.GetUnreviewedRevisionResp" + } + } + } + } + ] + } + } + } + ] + } + } + } + } + }, "/answer/api/v1/search": { "get": { "security": [ @@ -3311,10 +3550,7 @@ "type": "object", "properties": { "data": { - "type": "array", - "items": { - "$ref": "#/definitions/schema.GetTagSynonymsResp" - } + "$ref": "#/definitions/schema.GetTagSynonymsResp" } } } @@ -4536,6 +4772,67 @@ "list": {} } }, + "schema.ActObjectInfo": { + "type": "object", + "properties": { + "answer_id": { + "type": "string" + }, + "display_name": { + "type": "string" + }, + "object_type": { + "type": "string" + }, + "question_id": { + "type": "string" + }, + "title": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "schema.ActObjectTimeline": { + "type": "object", + "properties": { + "activity_id": { + "type": "string" + }, + "activity_type": { + "type": "string" + }, + "cancelled": { + "type": "boolean" + }, + "cancelled_at": { + "type": "integer" + }, + "comment": { + "type": "string" + }, + "created_at": { + "type": "integer" + }, + "object_id": { + "type": "string" + }, + "object_type": { + "type": "string" + }, + "revision_id": { + "type": "string" + }, + "user_display_name": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, "schema.ActionRecordResp": { "type": "object", "properties": { @@ -4646,7 +4943,7 @@ } } }, - "schema.AnswerList": { + "schema.AnswerListReq": { "type": "object", "properties": { "order": { @@ -4942,6 +5239,20 @@ } } }, + "schema.GetObjectTimelineResp": { + "type": "object", + "properties": { + "object_info": { + "$ref": "#/definitions/schema.ActObjectInfo" + }, + "timeline": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.ActObjectTimeline" + } + } + } + }, "schema.GetOtherUserInfoByUsernameResp": { "type": "object", "properties": { @@ -5330,21 +5641,33 @@ "schema.GetTagSynonymsResp": { "type": "object", "properties": { - "display_name": { - "description": "display name", + "member_actions": { + "description": "MemberActions", + "type": "array", + "items": { + "$ref": "#/definitions/schema.PermissionMemberAction" + } + }, + "synonyms": { + "description": "synonyms", + "type": "array", + "items": { + "$ref": "#/definitions/schema.TagSynonym" + } + } + } + }, + "schema.GetUnreviewedRevisionResp": { + "type": "object", + "properties": { + "info": { + "$ref": "#/definitions/schema.UnreviewedRevisionInfoInfo" + }, + "type": { "type": "string" }, - "main_tag_slug_name": { - "description": "if main tag slug name is not empty, this tag is synonymous with the main tag", - "type": "string" - }, - "slug_name": { - "description": "slug name", - "type": "string" - }, - "tag_id": { - "description": "tag id", - "type": "string" + "unreviewed_info": { + "$ref": "#/definitions/schema.GetRevisionResp" } } }, @@ -5848,6 +6171,23 @@ } } }, + "schema.RevisionAuditReq": { + "type": "object", + "required": [ + "id", + "operation" + ], + "properties": { + "id": { + "description": "object id", + "type": "string" + }, + "operation": { + "description": "approve or reject", + "type": "string" + } + } + }, "schema.SearchListResp": { "type": "object", "properties": { @@ -5977,9 +6317,7 @@ "type": "object", "required": [ "contact_email", - "description", "name", - "short_description", "site_url" ], "properties": { @@ -6009,9 +6347,7 @@ "type": "object", "required": [ "contact_email", - "description", "name", - "short_description", "site_url" ], "properties": { @@ -6199,6 +6535,50 @@ } } }, + "schema.TagSynonym": { + "type": "object", + "properties": { + "display_name": { + "description": "display name", + "type": "string" + }, + "main_tag_slug_name": { + "description": "if main tag slug name is not empty, this tag is synonymous with the main tag", + "type": "string" + }, + "slug_name": { + "description": "slug name", + "type": "string" + }, + "tag_id": { + "description": "tag id", + "type": "string" + } + } + }, + "schema.UnreviewedRevisionInfoInfo": { + "type": "object", + "properties": { + "content": { + "type": "string" + }, + "html": { + "type": "string" + }, + "object_id": { + "type": "string" + }, + "tags": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.TagResp" + } + }, + "title": { + "type": "string" + } + } + }, "schema.UpdateCommentReq": { "type": "object", "required": [ diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 6dc21985..5014b7ba 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -89,6 +89,46 @@ definitions: type: integer list: {} type: object + schema.ActObjectInfo: + properties: + answer_id: + type: string + display_name: + type: string + object_type: + type: string + question_id: + type: string + title: + type: string + username: + type: string + type: object + schema.ActObjectTimeline: + properties: + activity_id: + type: string + activity_type: + type: string + cancelled: + type: boolean + cancelled_at: + type: integer + comment: + type: string + created_at: + type: integer + object_id: + type: string + object_type: + type: string + revision_id: + type: string + user_display_name: + type: string + username: + type: string + type: object schema.ActionRecordResp: properties: captcha_id: @@ -166,7 +206,7 @@ definitions: description: question_id type: string type: object - schema.AnswerList: + schema.AnswerListReq: properties: order: description: 1 Default 2 time @@ -379,6 +419,15 @@ definitions: description: tag id type: string type: object + schema.GetObjectTimelineResp: + properties: + object_info: + $ref: '#/definitions/schema.ActObjectInfo' + timeline: + items: + $ref: '#/definitions/schema.ActObjectTimeline' + type: array + type: object schema.GetOtherUserInfoByUsernameResp: properties: answer_count: @@ -659,19 +708,25 @@ definitions: type: object schema.GetTagSynonymsResp: properties: - display_name: - description: display name - type: string - main_tag_slug_name: - description: if main tag slug name is not empty, this tag is synonymous with - the main tag - type: string - slug_name: - description: slug name - type: string - tag_id: - description: tag id + member_actions: + description: MemberActions + items: + $ref: '#/definitions/schema.PermissionMemberAction' + type: array + synonyms: + description: synonyms + items: + $ref: '#/definitions/schema.TagSynonym' + type: array + type: object + schema.GetUnreviewedRevisionResp: + properties: + info: + $ref: '#/definitions/schema.UnreviewedRevisionInfoInfo' + type: type: string + unreviewed_info: + $ref: '#/definitions/schema.GetRevisionResp' type: object schema.GetUserPageResp: properties: @@ -1039,6 +1094,18 @@ definitions: - flagged_type - id type: object + schema.RevisionAuditReq: + properties: + id: + description: object id + type: string + operation: + description: approve or reject + type: string + required: + - id + - operation + type: object schema.SearchListResp: properties: count: @@ -1145,9 +1212,7 @@ definitions: type: string required: - contact_email - - description - name - - short_description - site_url type: object schema.SiteGeneralResp: @@ -1169,9 +1234,7 @@ definitions: type: string required: - contact_email - - description - name - - short_description - site_url type: object schema.SiteInterfaceReq: @@ -1286,6 +1349,37 @@ definitions: slug_name: type: string type: object + schema.TagSynonym: + properties: + display_name: + description: display name + type: string + main_tag_slug_name: + description: if main tag slug name is not empty, this tag is synonymous with + the main tag + type: string + slug_name: + description: slug name + type: string + tag_id: + description: tag id + type: string + type: object + schema.UnreviewedRevisionInfoInfo: + properties: + content: + type: string + html: + type: string + object_id: + type: string + tags: + items: + $ref: '#/definitions/schema.TagResp' + type: array + title: + type: string + type: object schema.UpdateCommentReq: properties: comment_id: @@ -2264,6 +2358,69 @@ paths: summary: get user page tags: - admin + /answer/api/v1/activity/timeline: + get: + description: get object timeline + parameters: + - description: object id + in: query + name: object_id + type: string + - description: tag slug name + in: query + name: tag_slug_name + type: string + - description: object type + enum: + - question + - answer + - tag + in: query + name: object_type + type: string + - description: is show vote + in: query + name: show_vote + type: boolean + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/handler.RespBody' + - properties: + data: + $ref: '#/definitions/schema.GetObjectTimelineResp' + type: object + summary: get object timeline + tags: + - Comment + /answer/api/v1/activity/timeline/detail: + get: + description: get object timeline detail + parameters: + - description: revision id + in: query + name: revision_id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/handler.RespBody' + - properties: + data: + $ref: '#/definitions/schema.GetObjectTimelineResp' + type: object + summary: get object timeline detail + tags: + - Comment /answer/api/v1/answer: delete: consumes: @@ -2386,12 +2543,12 @@ paths: - application/json description: AnswerList <br> <b>order</b> (default or updated) parameters: - - description: AnswerList + - description: AnswerListReq in: body name: data required: true schema: - $ref: '#/definitions/schema.AnswerList' + $ref: '#/definitions/schema.AnswerListReq' produces: - application/json responses: @@ -3437,6 +3594,85 @@ paths: summary: get revision list tags: - Revision + /answer/api/v1/revisions/audit: + put: + description: revision audit operation:approve or reject + parameters: + - description: audit + in: body + name: data + required: true + schema: + $ref: '#/definitions/schema.RevisionAuditReq' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handler.RespBody' + security: + - ApiKeyAuth: [] + summary: revision audit + tags: + - Revision + /answer/api/v1/revisions/edit/check: + get: + consumes: + - application/json + description: check can update revision + parameters: + - default: string + description: id + in: query + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handler.RespBody' + security: + - ApiKeyAuth: [] + summary: check can update revision + tags: + - Revision + /answer/api/v1/revisions/unreviewed: + get: + description: get unreviewed revision list + parameters: + - description: page id + in: query + name: page + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/handler.RespBody' + - properties: + data: + allOf: + - $ref: '#/definitions/pager.PageModel' + - properties: + list: + items: + $ref: '#/definitions/schema.GetUnreviewedRevisionResp' + type: array + type: object + type: object + security: + - ApiKeyAuth: [] + summary: get unreviewed revision list + tags: + - Revision /answer/api/v1/search: get: description: search object @@ -3632,9 +3868,7 @@ paths: - $ref: '#/definitions/handler.RespBody' - properties: data: - items: - $ref: '#/definitions/schema.GetTagSynonymsResp' - type: array + $ref: '#/definitions/schema.GetTagSynonymsResp' type: object summary: get tag synonyms tags: diff --git a/go.mod b/go.mod index 95796a93..13bd9f47 100644 --- a/go.mod +++ b/go.mod @@ -65,6 +65,7 @@ require ( github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect github.com/golang/snappy v0.0.4 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect + github.com/google/subcommands v1.0.1 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/imdario/mergo v0.3.12 // indirect github.com/inconshreveable/mousetrap v1.0.1 // indirect @@ -105,6 +106,7 @@ require ( go.uber.org/multierr v1.8.0 // indirect go.uber.org/zap v1.23.0 // indirect golang.org/x/image v0.1.0 // indirect + golang.org/x/mod v0.6.0 // indirect golang.org/x/sys v0.1.0 // indirect golang.org/x/text v0.4.0 // indirect golang.org/x/tools v0.2.0 // indirect diff --git a/go.sum b/go.sum index f12aeb7a..8bfba639 100644 --- a/go.sum +++ b/go.sum @@ -284,6 +284,7 @@ github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLe github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= +github.com/google/subcommands v1.0.1 h1:/eqq+otEXm5vhfBrbREPCSVQbvofip6kIz+mX5TUH7k= github.com/google/subcommands v1.0.1/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -779,6 +780,7 @@ golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.6.0 h1:b9gGHsz9/HhJ3HF5DHQytPpuwocVTChQJK3AvoLRD5I= +golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= diff --git a/i18n/en_US.yaml b/i18n/en_US.yaml index fe82b20d..a572c4a8 100644 --- a/i18n/en_US.yaml +++ b/i18n/en_US.yaml @@ -1,4 +1,5 @@ # The following fields are used for back-end + backend: base: success: @@ -42,6 +43,10 @@ backend: answer: not_found: other: "Answer do not found." + cannot_deleted: + other: "No permission to delete." + cannot_update: + other: "No permission to update." comment: edit_without_permission: other: "Comment are not allowed to edit." @@ -79,6 +84,12 @@ backend: question: not_found: other: "Question not found." + cannot_deleted: + other: "No permission to delete." + cannot_close: + other: "No permission to close." + cannot_update: + other: "No permission to update." rank: fail_to_meet_the_condition: other: "Rank fail to meet the condition." @@ -92,11 +103,20 @@ backend: other: "Tag not found." recommend_tag_not_found: other: "Recommend Tag is not exist." + recommend_tag_enter: + other: "Please enter at least one required tag." not_contain_synonym_tags: other: "Should not contain synonym tags." + cannot_update: + other: "No permission to update." theme: not_found: other: "Theme not found." + revision: + review_underway: + other: "Can't edit currently, there is a version in the review queue." + no_permission: + other: "No permission to Revision." user: email_or_password_wrong: other: *email_or_password_wrong @@ -110,18 +130,17 @@ backend: other: "Username is already in use." set_avatar: other: "Avatar set failed." - - config: - read_config_failed: - other: "Read config failed" - database: - connection_failed: - other: "Database connection failed" - create_table_failed: - other: "Create table failed" - install: - create_config_failed: - other: "Can’t create the config.yaml file." + config: + read_config_failed: + other: "Read config failed" + database: + connection_failed: + other: "Database connection failed" + create_table_failed: + other: "Create table failed" + install: + create_config_failed: + other: "Can’t create the config.yaml file." report: spam: name: diff --git a/internal/base/constant/acticity.go b/internal/base/constant/acticity.go new file mode 100644 index 00000000..befa016b --- /dev/null +++ b/internal/base/constant/acticity.go @@ -0,0 +1,44 @@ +package constant + +// question activity + +type ActivityTypeKey string + +const ( + ActQuestionAsked ActivityTypeKey = "question.asked" + ActQuestionClosed ActivityTypeKey = "question.closed" + ActQuestionReopened ActivityTypeKey = "question.reopened" + ActQuestionAnswered ActivityTypeKey = "question.answered" + ActQuestionCommented ActivityTypeKey = "question.commented" + ActQuestionAccept ActivityTypeKey = "question.accept" + ActQuestionUpvote ActivityTypeKey = "question.upvote" + ActQuestionDownvote ActivityTypeKey = "question.downvote" + ActQuestionEdited ActivityTypeKey = "question.edited" + ActQuestionRollback ActivityTypeKey = "question.rollback" + ActQuestionDeleted ActivityTypeKey = "question.deleted" + ActQuestionUndeleted ActivityTypeKey = "question.undeleted" +) + +// answer activity + +const ( + ActAnswerAnswered ActivityTypeKey = "answer.answered" + ActAnswerCommented ActivityTypeKey = "answer.commented" + ActAnswerAccept ActivityTypeKey = "answer.accept" + ActAnswerUpvote ActivityTypeKey = "answer.upvote" + ActAnswerDownvote ActivityTypeKey = "answer.downvote" + ActAnswerEdited ActivityTypeKey = "answer.edited" + ActAnswerRollback ActivityTypeKey = "answer.rollback" + ActAnswerDeleted ActivityTypeKey = "answer.deleted" + ActAnswerUndeleted ActivityTypeKey = "answer.undeleted" +) + +// tag activity + +const ( + ActTagCreated ActivityTypeKey = "tag.created" + ActTagEdited ActivityTypeKey = "tag.edited" + ActTagRollback ActivityTypeKey = "tag.rollback" + ActTagDeleted ActivityTypeKey = "tag.deleted" + ActTagUndeleted ActivityTypeKey = "tag.undeleted" +) diff --git a/internal/base/middleware/auth.go b/internal/base/middleware/auth.go index 813effe3..e793d3c7 100644 --- a/internal/base/middleware/auth.go +++ b/internal/base/middleware/auth.go @@ -113,15 +113,20 @@ func (am *AuthUserMiddleware) CmsAuth() gin.HandlerFunc { // GetLoginUserIDFromContext get user id from context func GetLoginUserIDFromContext(ctx *gin.Context) (userID string) { - userInfo, exist := ctx.Get(ctxUUIDKey) - if !exist { + userInfo := GetUserInfoFromContext(ctx) + if userInfo == nil { return "" } - u, ok := userInfo.(*entity.UserCacheInfo) - if !ok { - return "" + return userInfo.UserID +} + +// GetIsAdminFromContext get user is admin from context +func GetIsAdminFromContext(ctx *gin.Context) (isAdmin bool) { + userInfo := GetUserInfoFromContext(ctx) + if userInfo == nil { + return false } - return u.UserID + return userInfo.IsAdmin } // GetUserInfoFromContext get user info from context diff --git a/internal/base/reason/reason.go b/internal/base/reason/reason.go index 23e7bae2..21dbf341 100644 --- a/internal/base/reason/reason.go +++ b/internal/base/reason/reason.go @@ -14,38 +14,47 @@ const ( ) const ( - EmailOrPasswordWrong = "error.object.email_or_password_incorrect" - CommentNotFound = "error.comment.not_found" - QuestionNotFound = "error.question.not_found" - AnswerNotFound = "error.answer.not_found" - CommentEditWithoutPermission = "error.comment.edit_without_permission" - DisallowVote = "error.object.disallow_vote" - DisallowFollow = "error.object.disallow_follow" - DisallowVoteYourSelf = "error.object.disallow_vote_your_self" - CaptchaVerificationFailed = "error.object.captcha_verification_failed" + EmailOrPasswordWrong = "error.object.email_or_password_incorrect" + CommentNotFound = "error.comment.not_found" + QuestionNotFound = "error.question.not_found" + QuestionCannotDeleted = "error.question.cannot_deleted" + QuestionCannotClose = "error.question.cannot_close" + QuestionCannotUpdate = "error.question.cannot_update" + AnswerNotFound = "error.answer.not_found" + AnswerCannotDeleted = "error.answer.cannot_deleted" + AnswerCannotUpdate = "error.answer.cannot_update" + CommentEditWithoutPermission = "error.comment.edit_without_permission" + DisallowVote = "error.object.disallow_vote" + DisallowFollow = "error.object.disallow_follow" + DisallowVoteYourSelf = "error.object.disallow_vote_your_self" + CaptchaVerificationFailed = "error.object.captcha_verification_failed" OldPasswordVerificationFailed = "error.object.old_password_verification_failed" NewPasswordSameAsPreviousSetting = "error.object.new_password_same_as_previous_setting" - UserNotFound = "error.user.not_found" - UsernameInvalid = "error.user.username_invalid" - UsernameDuplicate = "error.user.username_duplicate" - UserSetAvatar = "error.user.set_avatar" - EmailDuplicate = "error.email.duplicate" - EmailVerifyURLExpired = "error.email.verify_url_expired" - EmailNeedToBeVerified = "error.email.need_to_be_verified" - UserSuspended = "error.user.suspended" - ObjectNotFound = "error.object.not_found" - TagNotFound = "error.tag.not_found" - TagNotContainSynonym = "error.tag.not_contain_synonym_tags" - RankFailToMeetTheCondition = "error.rank.fail_to_meet_the_condition" - ThemeNotFound = "error.theme.not_found" - LangNotFound = "error.lang.not_found" - ReportHandleFailed = "error.report.handle_failed" - ReportNotFound = "error.report.not_found" - ReadConfigFailed = "error.config.read_config_failed" - DatabaseConnectionFailed = "error.database.connection_failed" - InstallCreateTableFailed = "error.database.create_table_failed" - InstallConfigFailed = "error.install.create_config_failed" - SiteInfoNotFound = "error.site_info.not_found" - UploadFileSourceUnsupported = "error.upload.source_unsupported" - RecommendTagNotExist = "error.tag.recommend_tag_not_found" + UserNotFound = "error.user.not_found" + UsernameInvalid = "error.user.username_invalid" + UsernameDuplicate = "error.user.username_duplicate" + UserSetAvatar = "error.user.set_avatar" + EmailDuplicate = "error.email.duplicate" + EmailVerifyURLExpired = "error.email.verify_url_expired" + EmailNeedToBeVerified = "error.email.need_to_be_verified" + UserSuspended = "error.user.suspended" + ObjectNotFound = "error.object.not_found" + TagNotFound = "error.tag.not_found" + TagNotContainSynonym = "error.tag.not_contain_synonym_tags" + TagCannotUpdate = "error.tag.cannot_update" + RankFailToMeetTheCondition = "error.rank.fail_to_meet_the_condition" + ThemeNotFound = "error.theme.not_found" + LangNotFound = "error.lang.not_found" + ReportHandleFailed = "error.report.handle_failed" + ReportNotFound = "error.report.not_found" + ReadConfigFailed = "error.config.read_config_failed" + DatabaseConnectionFailed = "error.database.connection_failed" + InstallCreateTableFailed = "error.database.create_table_failed" + InstallConfigFailed = "error.install.create_config_failed" + SiteInfoNotFound = "error.site_info.not_found" + UploadFileSourceUnsupported = "error.upload.source_unsupported" + RecommendTagNotExist = "error.tag.recommend_tag_not_found" + RecommendTagEnter = "error.tag.recommend_tag_enter" + RevisionReviewUnderway = "error.revision.review_underway" + RevisionNoPermission = "error.revision.no_permission" ) diff --git a/internal/controller/activity_controller.go b/internal/controller/activity_controller.go new file mode 100644 index 00000000..df49a4c8 --- /dev/null +++ b/internal/controller/activity_controller.go @@ -0,0 +1,65 @@ +package controller + +import ( + "github.com/answerdev/answer/internal/base/handler" + "github.com/answerdev/answer/internal/base/middleware" + "github.com/answerdev/answer/internal/schema" + "github.com/answerdev/answer/internal/service/activity" + "github.com/answerdev/answer/internal/service/activity_common" + "github.com/gin-gonic/gin" +) + +type ActivityController struct { + activityCommonService *activity_common.ActivityCommon + activityService *activity.ActivityService +} + +// NewActivityController new activity controller. +func NewActivityController( + activityCommonService *activity_common.ActivityCommon, + activityService *activity.ActivityService) *ActivityController { + return &ActivityController{activityCommonService: activityCommonService, activityService: activityService} +} + +// GetObjectTimeline get object timeline +// @Summary get object timeline +// @Description get object timeline +// @Tags Comment +// @Produce json +// @Param object_id query string false "object id" +// @Param tag_slug_name query string false "tag slug name" +// @Param object_type query string false "object type" Enums(question, answer, tag) +// @Param show_vote query boolean false "is show vote" +// @Success 200 {object} handler.RespBody{data=schema.GetObjectTimelineResp} +// @Router /answer/api/v1/activity/timeline [get] +func (ac *ActivityController) GetObjectTimeline(ctx *gin.Context) { + req := &schema.GetObjectTimelineReq{} + if handler.BindAndCheck(ctx, req) { + return + } + + req.UserID = middleware.GetLoginUserIDFromContext(ctx) + + resp, err := ac.activityService.GetObjectTimeline(ctx, req) + handler.HandleResponse(ctx, err, resp) +} + +// GetObjectTimelineDetail get object timeline detail +// @Summary get object timeline detail +// @Description get object timeline detail +// @Tags Comment +// @Produce json +// @Param revision_id query string true "revision id" +// @Success 200 {object} handler.RespBody{data=schema.GetObjectTimelineResp} +// @Router /answer/api/v1/activity/timeline/detail [get] +func (ac *ActivityController) GetObjectTimelineDetail(ctx *gin.Context) { + req := &schema.GetObjectTimelineDetailReq{} + if handler.BindAndCheck(ctx, req) { + return + } + + req.UserID = middleware.GetLoginUserIDFromContext(ctx) + + resp, err := ac.activityService.GetObjectTimelineDetail(ctx, req) + handler.HandleResponse(ctx, err, resp) +} diff --git a/internal/controller/answer_controller.go b/internal/controller/answer_controller.go index 6db77008..615dd639 100644 --- a/internal/controller/answer_controller.go +++ b/internal/controller/answer_controller.go @@ -51,12 +51,18 @@ func (ac *AnswerController) RemoveAnswer(ctx *gin.Context) { } req.UserID = middleware.GetLoginUserIDFromContext(ctx) - if can, err := ac.rankService.CheckRankPermission(ctx, req.UserID, rank.AnswerDeleteRank); err != nil || !can { - handler.HandleResponse(ctx, err, errors.Forbidden(reason.RankFailToMeetTheCondition)) + req.IsAdmin = middleware.GetIsAdminFromContext(ctx) + can, err := ac.rankService.CheckOperationPermission(ctx, req.UserID, rank.AnswerDeleteRank, req.ID) + if err != nil { + handler.HandleResponse(ctx, err, nil) + return + } + if !can { + handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil) return } - err := ac.answerService.RemoveAnswer(ctx, req) + err = ac.answerService.RemoveAnswer(ctx, req) handler.HandleResponse(ctx, err, nil) } @@ -105,8 +111,13 @@ func (ac *AnswerController) Add(ctx *gin.Context) { } req.UserID = middleware.GetLoginUserIDFromContext(ctx) - if can, err := ac.rankService.CheckRankPermission(ctx, req.UserID, rank.AnswerAddRank); err != nil || !can { - handler.HandleResponse(ctx, err, errors.Forbidden(reason.RankFailToMeetTheCondition)) + can, err := ac.rankService.CheckOperationPermission(ctx, req.UserID, rank.AnswerAddRank, "") + if err != nil { + handler.HandleResponse(ctx, err, nil) + return + } + if !can { + handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil) return } @@ -148,30 +159,32 @@ func (ac *AnswerController) Update(ctx *gin.Context) { } req.UserID = middleware.GetLoginUserIDFromContext(ctx) - if can, err := ac.rankService.CheckRankPermission(ctx, req.UserID, rank.AnswerEditRank); err != nil || !can { - handler.HandleResponse(ctx, err, errors.Forbidden(reason.RankFailToMeetTheCondition)) + canList, err := ac.rankService.CheckOperationPermissions(ctx, req.UserID, []string{ + rank.AnswerEditRank, + rank.AnswerEditWithoutReviewRank, + }, "") + if err != nil { + handler.HandleResponse(ctx, err, nil) + return + } + req.CanEdit = canList[0] + req.NoNeedReview = canList[1] + if !req.CanEdit { + handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil) return } - _, err := ac.answerService.Update(ctx, req) + _, err = ac.answerService.Update(ctx, req) if err != nil { handler.HandleResponse(ctx, err, nil) return } - info, questionInfo, has, err := ac.answerService.Get(ctx, req.ID, req.UserID) + _, _, _, err = ac.answerService.Get(ctx, req.ID, req.UserID) if err != nil { handler.HandleResponse(ctx, err, nil) return } - if !has { - // todo !has - handler.HandleResponse(ctx, nil, nil) - return - } - handler.HandleResponse(ctx, nil, gin.H{ - "info": info, - "question": questionInfo, - }) + handler.HandleResponse(ctx, nil, &schema.AnswerUpdateResp{WaitForReview: !req.NoNeedReview}) } // AnswerList godoc @@ -181,15 +194,27 @@ func (ac *AnswerController) Update(ctx *gin.Context) { // @Security ApiKeyAuth // @Accept json // @Produce json -// @Param data body schema.AnswerList true "AnswerList" +// @Param data body schema.AnswerListReq true "AnswerListReq" // @Success 200 {string} string "" // @Router /answer/api/v1/answer/list [get] func (ac *AnswerController) AnswerList(ctx *gin.Context) { - req := &schema.AnswerList{} + req := &schema.AnswerListReq{} if handler.BindAndCheck(ctx, req) { return } - req.LoginUserID = middleware.GetLoginUserIDFromContext(ctx) + req.UserID = middleware.GetLoginUserIDFromContext(ctx) + + canList, err := ac.rankService.CheckOperationPermissions(ctx, req.UserID, []string{ + rank.AnswerEditRank, + rank.AnswerDeleteRank, + }, "") + if err != nil { + handler.HandleResponse(ctx, err, nil) + return + } + req.CanEdit = canList[0] + req.CanDelete = canList[1] + list, count, err := ac.answerService.SearchList(ctx, req) if err != nil { handler.HandleResponse(ctx, err, nil) @@ -218,12 +243,17 @@ func (ac *AnswerController) Adopted(ctx *gin.Context) { } req.UserID = middleware.GetLoginUserIDFromContext(ctx) - if can, err := ac.rankService.CheckRankPermission(ctx, req.UserID, rank.AnswerAcceptRank); err != nil || !can { - handler.HandleResponse(ctx, err, errors.Forbidden(reason.RankFailToMeetTheCondition)) + can, err := ac.rankService.CheckOperationPermission(ctx, req.UserID, rank.AnswerAcceptRank, req.QuestionID) + if err != nil { + handler.HandleResponse(ctx, err, nil) + return + } + if !can { + handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil) return } - err := ac.answerService.UpdateAdopted(ctx, req) + err = ac.answerService.UpdateAdopted(ctx, req) handler.HandleResponse(ctx, err, nil) } diff --git a/internal/controller/comment_controller.go b/internal/controller/comment_controller.go index aad50ed2..b7a161e1 100644 --- a/internal/controller/comment_controller.go +++ b/internal/controller/comment_controller.go @@ -41,8 +41,20 @@ func (cc *CommentController) AddComment(ctx *gin.Context) { } req.UserID = middleware.GetLoginUserIDFromContext(ctx) - if can, err := cc.rankService.CheckRankPermission(ctx, req.UserID, rank.CommentAddRank); err != nil || !can { - handler.HandleResponse(ctx, err, errors.Forbidden(reason.RankFailToMeetTheCondition)) + canList, err := cc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{ + rank.CommentAddRank, + rank.CommentEditRank, + rank.CommentDeleteRank, + }, "") + if err != nil { + handler.HandleResponse(ctx, err, nil) + return + } + req.CanAdd = canList[0] + req.CanEdit = canList[1] + req.CanDelete = canList[2] + if !req.CanAdd { + handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil) return } @@ -67,12 +79,17 @@ func (cc *CommentController) RemoveComment(ctx *gin.Context) { } req.UserID = middleware.GetLoginUserIDFromContext(ctx) - if can, err := cc.rankService.CheckRankPermission(ctx, req.UserID, rank.CommentDeleteRank); err != nil || !can { - handler.HandleResponse(ctx, err, errors.Forbidden(reason.RankFailToMeetTheCondition)) + can, err := cc.rankService.CheckOperationPermission(ctx, req.UserID, rank.CommentDeleteRank, req.CommentID) + if err != nil { + handler.HandleResponse(ctx, err, nil) + return + } + if !can { + handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil) return } - err := cc.commentService.RemoveComment(ctx, req) + err = cc.commentService.RemoveComment(ctx, req) handler.HandleResponse(ctx, err, nil) } @@ -93,12 +110,17 @@ func (cc *CommentController) UpdateComment(ctx *gin.Context) { } req.UserID = middleware.GetLoginUserIDFromContext(ctx) - if can, err := cc.rankService.CheckRankPermission(ctx, req.UserID, rank.CommentEditRank); err != nil || !can { - handler.HandleResponse(ctx, err, errors.Forbidden(reason.RankFailToMeetTheCondition)) + can, err := cc.rankService.CheckOperationPermission(ctx, req.UserID, rank.CommentEditRank, req.CommentID) + if err != nil { + handler.HandleResponse(ctx, err, nil) + return + } + if !can { + handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil) return } - err := cc.commentService.UpdateComment(ctx, req) + err = cc.commentService.UpdateComment(ctx, req) handler.HandleResponse(ctx, err, nil) } @@ -120,6 +142,16 @@ func (cc *CommentController) GetCommentWithPage(ctx *gin.Context) { } req.UserID = middleware.GetLoginUserIDFromContext(ctx) + canList, err := cc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{ + rank.CommentEditRank, + rank.CommentDeleteRank, + }, "") + if err != nil { + handler.HandleResponse(ctx, err, nil) + return + } + req.CanEdit = canList[0] + req.CanDelete = canList[1] resp, err := cc.commentService.GetCommentWithPage(ctx, req) handler.HandleResponse(ctx, err, resp) @@ -162,6 +194,16 @@ func (cc *CommentController) GetComment(ctx *gin.Context) { } req.UserID = middleware.GetLoginUserIDFromContext(ctx) + canList, err := cc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{ + rank.CommentEditRank, + rank.CommentDeleteRank, + }, "") + if err != nil { + handler.HandleResponse(ctx, err, nil) + return + } + req.CanEdit = canList[0] + req.CanDelete = canList[1] resp, err := cc.commentService.GetComment(ctx, req) handler.HandleResponse(ctx, err, resp) diff --git a/internal/controller/controller.go b/internal/controller/controller.go index e23230b1..5b3e34f3 100644 --- a/internal/controller/controller.go +++ b/internal/controller/controller.go @@ -22,4 +22,5 @@ var ProviderSetController = wire.NewSet( NewSiteinfoController, NewDashboardController, NewUploadController, + NewActivityController, ) diff --git a/internal/controller/notification_controller.go b/internal/controller/notification_controller.go index 7c3afdae..91422555 100644 --- a/internal/controller/notification_controller.go +++ b/internal/controller/notification_controller.go @@ -5,17 +5,25 @@ import ( "github.com/answerdev/answer/internal/base/middleware" "github.com/answerdev/answer/internal/schema" "github.com/answerdev/answer/internal/service/notification" + "github.com/answerdev/answer/internal/service/rank" "github.com/gin-gonic/gin" ) // NotificationController notification controller type NotificationController struct { notificationService *notification.NotificationService + rankService *rank.RankService } // NewNotificationController new controller -func NewNotificationController(notificationService *notification.NotificationService) *NotificationController { - return &NotificationController{notificationService: notificationService} +func NewNotificationController( + notificationService *notification.NotificationService, + rankService *rank.RankService, +) *NotificationController { + return &NotificationController{ + notificationService: notificationService, + rankService: rankService, + } } // GetRedDot @@ -28,8 +36,26 @@ func NewNotificationController(notificationService *notification.NotificationSer // @Success 200 {object} handler.RespBody // @Router /answer/api/v1/notification/status [get] func (nc *NotificationController) GetRedDot(ctx *gin.Context) { + + req := &schema.GetRedDot{} + userID := middleware.GetLoginUserIDFromContext(ctx) - RedDot, err := nc.notificationService.GetRedDot(ctx, userID) + req.UserID = userID + req.UserID = middleware.GetLoginUserIDFromContext(ctx) + canList, err := nc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{ + rank.QuestionAuditRank, + rank.AnswerAuditRank, + rank.TagAuditRank, + }, "") + if err != nil { + handler.HandleResponse(ctx, err, nil) + return + } + req.CanReviewQuestion = canList[0] + req.CanReviewAnswer = canList[1] + req.CanReviewTag = canList[2] + + RedDot, err := nc.notificationService.GetRedDot(ctx, req) handler.HandleResponse(ctx, err, RedDot) } @@ -48,8 +74,21 @@ func (nc *NotificationController) ClearRedDot(ctx *gin.Context) { if handler.BindAndCheck(ctx, req) { return } - userID := middleware.GetLoginUserIDFromContext(ctx) - RedDot, err := nc.notificationService.ClearRedDot(ctx, userID, req.TypeStr) + req.UserID = middleware.GetLoginUserIDFromContext(ctx) + canList, err := nc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{ + rank.QuestionAuditRank, + rank.AnswerAuditRank, + rank.TagAuditRank, + }, "") + if err != nil { + handler.HandleResponse(ctx, err, nil) + return + } + req.CanReviewQuestion = canList[0] + req.CanReviewAnswer = canList[1] + req.CanReviewTag = canList[2] + + RedDot, err := nc.notificationService.ClearRedDot(ctx, req) handler.HandleResponse(ctx, err, RedDot) } diff --git a/internal/controller/question_controller.go b/internal/controller/question_controller.go index 281a7e1b..2fe17320 100644 --- a/internal/controller/question_controller.go +++ b/internal/controller/question_controller.go @@ -42,12 +42,18 @@ func (qc *QuestionController) RemoveQuestion(ctx *gin.Context) { return } req.UserID = middleware.GetLoginUserIDFromContext(ctx) - if can, err := qc.rankService.CheckRankPermission(ctx, req.UserID, rank.QuestionDeleteRank); err != nil || !can { - handler.HandleResponse(ctx, err, errors.Forbidden(reason.RankFailToMeetTheCondition)) + req.IsAdmin = middleware.GetIsAdminFromContext(ctx) + can, err := qc.rankService.CheckOperationPermission(ctx, req.UserID, rank.QuestionDeleteRank, req.ID) + if err != nil { + handler.HandleResponse(ctx, err, nil) + return + } + if !can { + handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil) return } - err := qc.questionService.RemoveQuestion(ctx, req) + err = qc.questionService.RemoveQuestion(ctx, req) handler.HandleResponse(ctx, err, nil) } @@ -67,6 +73,7 @@ func (qc *QuestionController) CloseQuestion(ctx *gin.Context) { return } req.UserID = middleware.GetLoginUserIDFromContext(ctx) + req.IsAdmin = middleware.GetIsAdminFromContext(ctx) err := qc.questionService.CloseQuestion(ctx, req) handler.HandleResponse(ctx, err, nil) } @@ -81,16 +88,28 @@ func (qc *QuestionController) CloseQuestion(ctx *gin.Context) { // @Param id query string true "Question TagID" default(1) // @Success 200 {string} string "" // @Router /answer/api/v1/question/info [get] -func (qc *QuestionController) GetQuestion(c *gin.Context) { - id := c.Query("id") - ctx := context.Background() - userID := middleware.GetLoginUserIDFromContext(c) - info, err := qc.questionService.GetQuestion(ctx, id, userID, true) +func (qc *QuestionController) GetQuestion(ctx *gin.Context) { + id := ctx.Query("id") + userID := middleware.GetLoginUserIDFromContext(ctx) + req := schema.QuestionPermission{} + canList, err := qc.rankService.CheckOperationPermissions(ctx, userID, []string{ + rank.QuestionEditRank, + rank.QuestionDeleteRank, + }, id) if err != nil { - handler.HandleResponse(c, err, nil) + handler.HandleResponse(ctx, err, nil) return } - handler.HandleResponse(c, nil, info) + req.CanEdit = canList[0] + req.CanDelete = canList[1] + req.CanClose = middleware.GetIsAdminFromContext(ctx) + + info, err := qc.questionService.GetQuestionAndAddPV(ctx, id, userID, req) + if err != nil { + handler.HandleResponse(ctx, err, nil) + return + } + handler.HandleResponse(ctx, nil, info) } // SimilarQuestion godoc @@ -188,8 +207,21 @@ func (qc *QuestionController) AddQuestion(ctx *gin.Context) { } req.UserID = middleware.GetLoginUserIDFromContext(ctx) - if can, err := qc.rankService.CheckRankPermission(ctx, req.UserID, rank.QuestionAddRank); err != nil || !can { - handler.HandleResponse(ctx, err, errors.Forbidden(reason.RankFailToMeetTheCondition)) + canList, err := qc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{ + rank.QuestionAddRank, + rank.QuestionEditRank, + rank.QuestionDeleteRank, + }, "") + if err != nil { + handler.HandleResponse(ctx, err, nil) + return + } + req.CanAdd = canList[0] + req.CanEdit = canList[1] + req.CanDelete = canList[2] + req.CanClose = middleware.GetIsAdminFromContext(ctx) + if !req.CanAdd { + handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil) return } @@ -214,13 +246,28 @@ func (qc *QuestionController) UpdateQuestion(ctx *gin.Context) { } req.UserID = middleware.GetLoginUserIDFromContext(ctx) - if can, err := qc.rankService.CheckRankPermission(ctx, req.UserID, rank.QuestionEditRank); err != nil || !can { - handler.HandleResponse(ctx, err, errors.Forbidden(reason.RankFailToMeetTheCondition)) + canList, err := qc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{ + rank.QuestionEditRank, + rank.QuestionDeleteRank, + rank.QuestionEditWithoutReviewRank, + }, req.ID) + if err != nil { + handler.HandleResponse(ctx, err, nil) + return + } + req.CanEdit = canList[0] + req.CanDelete = canList[1] + req.NoNeedReview = canList[2] + + req.CanClose = middleware.GetIsAdminFromContext(ctx) + req.IsAdmin = middleware.GetIsAdminFromContext(ctx) + if !req.CanEdit { + handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil) return } - resp, err := qc.questionService.UpdateQuestion(ctx, req) - handler.HandleResponse(ctx, err, resp) + _, err = qc.questionService.UpdateQuestion(ctx, req) + handler.HandleResponse(ctx, err, &schema.UpdateQuestionResp{WaitForReview: !req.NoNeedReview}) } // CloseMsgList close question msg list diff --git a/internal/controller/report_controller.go b/internal/controller/report_controller.go index 1067ee52..6e7141ec 100644 --- a/internal/controller/report_controller.go +++ b/internal/controller/report_controller.go @@ -40,12 +40,17 @@ func (rc *ReportController) AddReport(ctx *gin.Context) { } req.UserID = middleware.GetLoginUserIDFromContext(ctx) - if can, err := rc.rankService.CheckRankPermission(ctx, req.UserID, rank.ReportAddRank); err != nil || !can { - handler.HandleResponse(ctx, err, errors.Forbidden(reason.RankFailToMeetTheCondition)) + can, err := rc.rankService.CheckOperationPermission(ctx, req.UserID, rank.ReportAddRank, "") + if err != nil { + handler.HandleResponse(ctx, err, nil) + return + } + if !can { + handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil) return } - err := rc.reportService.AddReport(ctx, req) + err = rc.reportService.AddReport(ctx, req) handler.HandleResponse(ctx, err, nil) } diff --git a/internal/controller/revision_controller.go b/internal/controller/revision_controller.go index 5f30b70e..090209b2 100644 --- a/internal/controller/revision_controller.go +++ b/internal/controller/revision_controller.go @@ -1,10 +1,14 @@ package controller import ( + "github.com/answerdev/answer/internal/base/constant" "github.com/answerdev/answer/internal/base/handler" + "github.com/answerdev/answer/internal/base/middleware" "github.com/answerdev/answer/internal/base/reason" "github.com/answerdev/answer/internal/schema" "github.com/answerdev/answer/internal/service" + "github.com/answerdev/answer/internal/service/rank" + "github.com/answerdev/answer/pkg/obj" "github.com/gin-gonic/gin" "github.com/segmentfault/pacman/errors" ) @@ -12,11 +16,18 @@ import ( // RevisionController revision controller type RevisionController struct { revisionListService *service.RevisionService + rankService *rank.RankService } // NewRevisionController new controller -func NewRevisionController(revisionListService *service.RevisionService) *RevisionController { - return &RevisionController{revisionListService: revisionListService} +func NewRevisionController( + revisionListService *service.RevisionService, + rankService *rank.RankService, +) *RevisionController { + return &RevisionController{ + revisionListService: revisionListService, + rankService: rankService, + } } // GetRevisionList godoc @@ -41,3 +52,113 @@ func (rc *RevisionController) GetRevisionList(ctx *gin.Context) { resp, err := rc.revisionListService.GetRevisionList(ctx, req) handler.HandleResponse(ctx, err, resp) } + +// GetUnreviewedRevisionList godoc +// @Summary get unreviewed revision list +// @Description get unreviewed revision list +// @Tags Revision +// @Produce json +// @Security ApiKeyAuth +// @Param page query string true "page id" +// @Success 200 {object} handler.RespBody{data=pager.PageModel{list=[]schema.GetUnreviewedRevisionResp}} +// @Router /answer/api/v1/revisions/unreviewed [get] +func (rc *RevisionController) GetUnreviewedRevisionList(ctx *gin.Context) { + req := &schema.RevisionSearch{} + if handler.BindAndCheck(ctx, req) { + return + } + + req.UserID = middleware.GetLoginUserIDFromContext(ctx) + canList, err := rc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{ + rank.QuestionAuditRank, + rank.AnswerAuditRank, + rank.TagAuditRank, + }, "") + if err != nil { + handler.HandleResponse(ctx, err, nil) + return + } + req.CanReviewQuestion = canList[0] + req.CanReviewAnswer = canList[1] + req.CanReviewTag = canList[2] + + resp, err := rc.revisionListService.GetUnreviewedRevisionPage(ctx, req) + handler.HandleResponse(ctx, err, resp) +} + +// RevisionAudit godoc +// @Summary revision audit +// @Description revision audit operation:approve or reject +// @Tags Revision +// @Produce json +// @Security ApiKeyAuth +// @Param data body schema.RevisionAuditReq true "audit" +// @Success 200 {object} handler.RespBody{} +// @Router /answer/api/v1/revisions/audit [put] +func (rc *RevisionController) RevisionAudit(ctx *gin.Context) { + req := &schema.RevisionAuditReq{} + if handler.BindAndCheck(ctx, req) { + return + } + req.UserID = middleware.GetLoginUserIDFromContext(ctx) + canList, err := rc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{ + rank.QuestionAuditRank, + rank.AnswerAuditRank, + rank.TagAuditRank, + }, "") + if err != nil { + handler.HandleResponse(ctx, err, nil) + return + } + req.CanReviewQuestion = canList[0] + req.CanReviewAnswer = canList[1] + req.CanReviewTag = canList[2] + + err = rc.revisionListService.RevisionAudit(ctx, req) + handler.HandleResponse(ctx, err, gin.H{}) +} + +// CheckCanUpdateRevision check can update revision +// @Summary check can update revision +// @Description check can update revision +// @Tags Revision +// @Accept json +// @Produce json +// @Security ApiKeyAuth +// @Param id query string true "id" default(string) +// @Success 200 {object} handler.RespBody +// @Router /answer/api/v1/revisions/edit/check [get] +func (rc *RevisionController) CheckCanUpdateRevision(ctx *gin.Context) { + req := &schema.CheckCanQuestionUpdate{} + if handler.BindAndCheck(ctx, req) { + return + } + req.UserID = middleware.GetLoginUserIDFromContext(ctx) + + action := "" + objectTypeStr, _ := obj.GetObjectTypeStrByObjectID(req.ID) + switch objectTypeStr { + case constant.QuestionObjectType: + action = rank.QuestionEditRank + case constant.AnswerObjectType: + action = rank.AnswerEditRank + case constant.TagObjectType: + action = rank.TagEditRank + default: + handler.HandleResponse(ctx, errors.BadRequest(reason.ObjectNotFound), nil) + return + } + + can, err := rc.rankService.CheckOperationPermission(ctx, req.UserID, action, req.ID) + if err != nil { + handler.HandleResponse(ctx, err, nil) + return + } + if !can { + handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil) + return + } + + err = rc.revisionListService.CheckCanUpdateRevision(ctx, req) + handler.HandleResponse(ctx, err, nil) +} diff --git a/internal/controller/tag_controller.go b/internal/controller/tag_controller.go index 673a746c..1164f1d4 100644 --- a/internal/controller/tag_controller.go +++ b/internal/controller/tag_controller.go @@ -42,8 +42,7 @@ func (tc *TagController) SearchTagLike(ctx *gin.Context) { if handler.BindAndCheck(ctx, req) { return } - userinfo := middleware.GetUserInfoFromContext(ctx) - req.IsAdmin = userinfo.IsAdmin + req.IsAdmin = middleware.GetIsAdminFromContext(ctx) resp, err := tc.tagCommonService.SearchTagLike(ctx, req) handler.HandleResponse(ctx, err, resp) } @@ -64,12 +63,17 @@ func (tc *TagController) RemoveTag(ctx *gin.Context) { } req.UserID = middleware.GetLoginUserIDFromContext(ctx) - if can, err := tc.rankService.CheckRankPermission(ctx, req.UserID, rank.TagDeleteRank); err != nil || !can { - handler.HandleResponse(ctx, err, errors.Forbidden(reason.RankFailToMeetTheCondition)) + can, err := tc.rankService.CheckOperationPermission(ctx, req.UserID, rank.TagDeleteRank, "") + if err != nil { + handler.HandleResponse(ctx, err, nil) + return + } + if !can { + handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil) return } - err := tc.tagService.RemoveTag(ctx, req.TagID) + err = tc.tagService.RemoveTag(ctx, req.TagID) handler.HandleResponse(ctx, err, nil) } @@ -89,13 +93,26 @@ func (tc *TagController) UpdateTag(ctx *gin.Context) { } req.UserID = middleware.GetLoginUserIDFromContext(ctx) - if can, err := tc.rankService.CheckRankPermission(ctx, req.UserID, rank.TagEditRank); err != nil || !can { - handler.HandleResponse(ctx, err, errors.Forbidden(reason.RankFailToMeetTheCondition)) + canList, err := tc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{ + rank.TagEditRank, + rank.TagEditWithoutReviewRank, + }, "") + if err != nil { + handler.HandleResponse(ctx, err, nil) return } + if !canList[0] { + handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil) + return + } + req.NoNeedReview = canList[1] - err := tc.tagService.UpdateTag(ctx, req) - handler.HandleResponse(ctx, err, nil) + err = tc.tagService.UpdateTag(ctx, req) + if err != nil { + handler.HandleResponse(ctx, err, nil) + } else { + handler.HandleResponse(ctx, err, &schema.UpdateTagResp{WaitForReview: !req.NoNeedReview}) + } } // GetTagInfo get tag one @@ -115,6 +132,16 @@ func (tc *TagController) GetTagInfo(ctx *gin.Context) { } req.UserID = middleware.GetLoginUserIDFromContext(ctx) + canList, err := tc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{ + rank.TagEditRank, + rank.TagDeleteRank, + }, "") + if err != nil { + handler.HandleResponse(ctx, err, nil) + return + } + req.CanEdit = canList[0] + req.CanDelete = canList[1] resp, err := tc.tagService.GetTagInfo(ctx, req) handler.HandleResponse(ctx, err, resp) @@ -163,7 +190,7 @@ func (tc *TagController) GetFollowingTags(ctx *gin.Context) { // @Tags Tag // @Produce json // @Param tag_id query int true "tag id" -// @Success 200 {object} handler.RespBody{data=[]schema.GetTagSynonymsResp} +// @Success 200 {object} handler.RespBody{data=schema.GetTagSynonymsResp} // @Router /answer/api/v1/tag/synonyms [get] func (tc *TagController) GetTagSynonyms(ctx *gin.Context) { req := &schema.GetTagSynonymsReq{} @@ -171,6 +198,16 @@ func (tc *TagController) GetTagSynonyms(ctx *gin.Context) { return } + req.UserID = middleware.GetLoginUserIDFromContext(ctx) + canList, err := tc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{ + rank.TagSynonymRank, + }, "") + if err != nil { + handler.HandleResponse(ctx, err, nil) + return + } + req.CanEdit = canList[0] + resp, err := tc.tagService.GetTagSynonyms(ctx, req) handler.HandleResponse(ctx, err, resp) } @@ -191,11 +228,16 @@ func (tc *TagController) UpdateTagSynonym(ctx *gin.Context) { } req.UserID = middleware.GetLoginUserIDFromContext(ctx) - if can, err := tc.rankService.CheckRankPermission(ctx, req.UserID, rank.TagSynonymRank); err != nil || !can { - handler.HandleResponse(ctx, err, errors.Forbidden(reason.RankFailToMeetTheCondition)) + can, err := tc.rankService.CheckOperationPermission(ctx, req.UserID, rank.TagSynonymRank, "") + if err != nil { + handler.HandleResponse(ctx, err, nil) + return + } + if !can { + handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil) return } - err := tc.tagService.UpdateTagSynonym(ctx, req) + err = tc.tagService.UpdateTagSynonym(ctx, req) handler.HandleResponse(ctx, err, nil) } diff --git a/internal/controller/vote_controller.go b/internal/controller/vote_controller.go index 7fcdf232..21783c86 100644 --- a/internal/controller/vote_controller.go +++ b/internal/controller/vote_controller.go @@ -3,20 +3,24 @@ package controller import ( "github.com/answerdev/answer/internal/base/handler" "github.com/answerdev/answer/internal/base/middleware" + "github.com/answerdev/answer/internal/base/reason" "github.com/answerdev/answer/internal/schema" "github.com/answerdev/answer/internal/service" + "github.com/answerdev/answer/internal/service/rank" "github.com/gin-gonic/gin" "github.com/jinzhu/copier" + "github.com/segmentfault/pacman/errors" ) // VoteController activity controller type VoteController struct { VoteService *service.VoteService + rankService *rank.RankService } // NewVoteController new controller -func NewVoteController(voteService *service.VoteService) *VoteController { - return &VoteController{VoteService: voteService} +func NewVoteController(voteService *service.VoteService, rankService *rank.RankService) *VoteController { + return &VoteController{VoteService: voteService, rankService: rankService} } // VoteUp godoc @@ -34,9 +38,19 @@ func (vc *VoteController) VoteUp(ctx *gin.Context) { if handler.BindAndCheck(ctx, req) { return } + req.UserID = middleware.GetLoginUserIDFromContext(ctx) + can, err := vc.rankService.CheckVotePermission(ctx, req.UserID, req.ObjectID, true) + if err != nil { + handler.HandleResponse(ctx, err, nil) + return + } + if !can { + handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil) + return + } + dto := &schema.VoteDTO{} _ = copier.Copy(dto, req) - dto.UserID = middleware.GetLoginUserIDFromContext(ctx) resp, err := vc.VoteService.VoteUp(ctx, dto) if err != nil { handler.HandleResponse(ctx, err, schema.ErrTypeToast) @@ -60,10 +74,20 @@ func (vc *VoteController) VoteDown(ctx *gin.Context) { if handler.BindAndCheck(ctx, req) { return } + + req.UserID = middleware.GetLoginUserIDFromContext(ctx) + can, err := vc.rankService.CheckVotePermission(ctx, req.UserID, req.ObjectID, false) + if err != nil { + handler.HandleResponse(ctx, err, nil) + return + } + if !can { + handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil) + return + } + dto := &schema.VoteDTO{} _ = copier.Copy(dto, req) - - dto.UserID = middleware.GetLoginUserIDFromContext(ctx) resp, err := vc.VoteService.VoteDown(ctx, dto) if err != nil { handler.HandleResponse(ctx, err, schema.ErrTypeToast) diff --git a/internal/entity/activity_entity.go b/internal/entity/activity_entity.go index 3e7a7c16..4db4eadd 100644 --- a/internal/entity/activity_entity.go +++ b/internal/entity/activity_entity.go @@ -9,16 +9,19 @@ const ( // Activity activity type Activity struct { - ID string `xorm:"not null pk autoincr BIGINT(20) id"` - CreatedAt time.Time `xorm:"created TIMESTAMP created_at"` - UpdatedAt time.Time `xorm:"updated TIMESTAMP updated_at"` - UserID string `xorm:"not null index BIGINT(20) user_id"` - TriggerUserID int64 `xorm:"not null default 0 index BIGINT(20) trigger_user_id"` - ObjectID string `xorm:"not null default 0 index BIGINT(20) object_id"` - ActivityType int `xorm:"not null INT(11) activity_type"` - Cancelled int `xorm:"not null default 0 TINYINT(4) cancelled"` - Rank int `xorm:"not null default 0 INT(11) rank"` - HasRank int `xorm:"not null default 0 TINYINT(4) has_rank"` + ID string `xorm:"not null pk autoincr BIGINT(20) id"` + CreatedAt time.Time `xorm:"created TIMESTAMP created_at"` + UpdatedAt time.Time `xorm:"updated TIMESTAMP updated_at"` + CancelledAt time.Time `xorm:"TIMESTAMP cancelled_at"` + UserID string `xorm:"not null index BIGINT(20) user_id"` + TriggerUserID int64 `xorm:"not null default 0 index BIGINT(20) trigger_user_id"` + ObjectID string `xorm:"not null default 0 index BIGINT(20) object_id"` + OriginalObjectID string `xorm:"not null default 0 BIGINT(20) original_object_id"` + ActivityType int `xorm:"not null INT(11) activity_type"` + Cancelled int `xorm:"not null default 0 TINYINT(4) cancelled"` + Rank int `xorm:"not null default 0 INT(11) rank"` + HasRank int `xorm:"not null default 0 TINYINT(4) has_rank"` + RevisionID int64 `xorm:"not null default 0 BIGINT(20) revision_id"` } type ActivityRankSum struct { diff --git a/internal/entity/answer_entity.go b/internal/entity/answer_entity.go index c51ae157..8e4d89e6 100644 --- a/internal/entity/answer_entity.go +++ b/internal/entity/answer_entity.go @@ -18,18 +18,19 @@ var CmsAnswerSearchStatus = map[string]int{ // Answer answer type Answer struct { - ID string `xorm:"not null pk autoincr BIGINT(20) id"` - CreatedAt time.Time `xorm:"created not null default CURRENT_TIMESTAMP TIMESTAMP created_at"` - UpdatedAt time.Time `xorm:"not null default CURRENT_TIMESTAMP TIMESTAMP updated_at"` - QuestionID string `xorm:"not null default 0 BIGINT(20) question_id"` - UserID string `xorm:"not null default 0 BIGINT(20) INDEX user_id"` - OriginalText string `xorm:"not null MEDIUMTEXT original_text"` - ParsedText string `xorm:"not null MEDIUMTEXT parsed_text"` - Status int `xorm:"not null default 1 INT(11) status"` - Adopted int `xorm:"not null default 1 INT(11) adopted"` - CommentCount int `xorm:"not null default 0 INT(11) comment_count"` - VoteCount int `xorm:"not null default 0 INT(11) vote_count"` - RevisionID string `xorm:"not null default 0 BIGINT(20) revision_id"` + ID string `xorm:"not null pk autoincr BIGINT(20) id"` + CreatedAt time.Time `xorm:"created not null default CURRENT_TIMESTAMP TIMESTAMP created_at"` + UpdatedAt time.Time `xorm:"updated_at TIMESTAMP"` + QuestionID string `xorm:"not null default 0 BIGINT(20) question_id"` + UserID string `xorm:"not null default 0 BIGINT(20) INDEX user_id"` + LastEditUserID string `xorm:"not null default 0 BIGINT(20) last_edit_user_id"` + OriginalText string `xorm:"not null MEDIUMTEXT original_text"` + ParsedText string `xorm:"not null MEDIUMTEXT parsed_text"` + Status int `xorm:"not null default 1 INT(11) status"` + Adopted int `xorm:"not null default 1 INT(11) adopted"` + CommentCount int `xorm:"not null default 0 INT(11) comment_count"` + VoteCount int `xorm:"not null default 0 INT(11) vote_count"` + RevisionID string `xorm:"not null default 0 BIGINT(20) revision_id"` } type AnswerSearch struct { diff --git a/internal/entity/question_entity.go b/internal/entity/question_entity.go index 83de9520..804b345d 100644 --- a/internal/entity/question_entity.go +++ b/internal/entity/question_entity.go @@ -6,19 +6,19 @@ import ( const ( QuestionStatusAvailable = 1 - QuestionStatusclosed = 2 + QuestionStatusClosed = 2 QuestionStatusDeleted = 10 ) var CmsQuestionSearchStatus = map[string]int{ "available": QuestionStatusAvailable, - "closed": QuestionStatusclosed, + "closed": QuestionStatusClosed, "deleted": QuestionStatusDeleted, } var CmsQuestionSearchStatusIntToString = map[int]string{ QuestionStatusAvailable: "available", - QuestionStatusclosed: "closed", + QuestionStatusClosed: "closed", QuestionStatusDeleted: "deleted", } @@ -31,8 +31,9 @@ type QuestionTag struct { type Question struct { ID string `xorm:"not null pk BIGINT(20) id"` CreatedAt time.Time `xorm:"not null default CURRENT_TIMESTAMP TIMESTAMP created_at"` - UpdatedAt time.Time `xorm:"not null default CURRENT_TIMESTAMP TIMESTAMP updated_at"` + UpdatedAt time.Time `xorm:"updated_at TIMESTAMP"` UserID string `xorm:"not null default 0 BIGINT(20) INDEX user_id"` + LastEditUserID string `xorm:"not null default 0 BIGINT(20) last_edit_user_id"` Title string `xorm:"not null default '' VARCHAR(150) title"` OriginalText string `xorm:"not null MEDIUMTEXT original_text"` ParsedText string `xorm:"not null MEDIUMTEXT parsed_text"` @@ -45,11 +46,28 @@ type Question struct { FollowCount int `xorm:"not null default 0 INT(11) follow_count"` AcceptedAnswerID string `xorm:"not null default 0 BIGINT(20) accepted_answer_id"` LastAnswerID string `xorm:"not null default 0 BIGINT(20) last_answer_id"` - PostUpdateTime time.Time `xorm:"default CURRENT_TIMESTAMP TIMESTAMP post_update_time"` - RevisionID string `xorm:"not null default 0 BIGINT(20) revision_id"` + PostUpdateTime time.Time `xorm:"post_update_time TIMESTAMP"` + RevisionID string `xorm:"not null default 0 BIGINT(20) revision_id"` } // TableName question table name func (Question) TableName() string { return "question" } + +// QuestionWithTagsRevision question +type QuestionWithTagsRevision struct { + Question + Tags []*TagSimpleInfoForRevision `json:"tags"` +} + +// TagSimpleInfoForRevision tag simple info for revision +type TagSimpleInfoForRevision struct { + ID string `xorm:"not null pk comment('tag_id') BIGINT(20) id"` + MainTagID int64 `xorm:"not null default 0 BIGINT(20) main_tag_id"` + SlugName string `xorm:"not null default '' unique VARCHAR(35) slug_name"` + DisplayName string `xorm:"not null default '' VARCHAR(35) display_name"` + Recommend bool `xorm:"not null default false BOOL recommend"` + Reserved bool `xorm:"not null default false BOOL reserved"` + RevisionID string `xorm:"not null default 0 BIGINT(20) revision_id"` +} diff --git a/internal/entity/revision_entity.go b/internal/entity/revision_entity.go index 48740ef2..46bfd228 100644 --- a/internal/entity/revision_entity.go +++ b/internal/entity/revision_entity.go @@ -1,20 +1,31 @@ package entity -import "time" +import ( + "time" +) + +const ( + // RevisionUnreviewedStatus this revision is unreviewed + RevisionUnreviewedStatus = 1 + // RevisionReviewPassStatus this revision is reviewed and approved by operator + RevisionReviewPassStatus = 2 + // RevisionReviewRejectStatus this revision is reviewed and rejected by operator + RevisionReviewRejectStatus = 3 +) // Revision revision type Revision struct { - ID string `xorm:"not null pk autoincr BIGINT(20) id"` - CreatedAt time.Time `xorm:"created TIMESTAMP created_at"` - UpdatedAt time.Time `xorm:"updated TIMESTAMP updated_at"` - UserID string `xorm:"not null default 0 BIGINT(20) user_id"` - ObjectType int `xorm:"not null default 0 ) INT(11) object_type"` - ObjectID string `xorm:"not null default 0 BIGINT(20) INDEX object_id"` - Title string `xorm:"not null default '' VARCHAR(255) title"` - Content string `xorm:"not null TEXT content"` - Log string `xorm:"VARCHAR(255) log"` - // Status todo: this field is not used, will be removed in the future - Status int `xorm:"not null default 1 INT(11) status"` + ID string `xorm:"not null pk autoincr BIGINT(20) id"` + CreatedAt time.Time `xorm:"created TIMESTAMP created_at"` + UpdatedAt time.Time `xorm:"updated TIMESTAMP updated_at"` + UserID string `xorm:"not null default 0 BIGINT(20) user_id"` + ObjectType int `xorm:"not null default 0 INT(11) object_type"` + ObjectID string `xorm:"not null default 0 BIGINT(20) INDEX object_id"` + Title string `xorm:"not null default '' VARCHAR(255) title"` + Content string `xorm:"not null TEXT content"` + Log string `xorm:"VARCHAR(255) log"` + Status int `xorm:"not null default 1 INT(11) status"` + ReviewUserID int64 `xorm:"not null default 0 BIGINT(20) review_user_id"` } // TableName revision table name diff --git a/internal/entity/tag_entity.go b/internal/entity/tag_entity.go index 4d934e2d..2aa4dbac 100644 --- a/internal/entity/tag_entity.go +++ b/internal/entity/tag_entity.go @@ -24,6 +24,7 @@ type Tag struct { Recommend bool `xorm:"not null default false BOOL recommend"` Reserved bool `xorm:"not null default false BOOL reserved"` RevisionID string `xorm:"not null default 0 BIGINT(20) revision_id"` + UserID string `xorm:"not null default 0 BIGINT(20) user_id"` } // TableName tag table name diff --git a/internal/install/install_controller.go b/internal/install/install_controller.go index 6963e44b..62cbdf87 100644 --- a/internal/install/install_controller.go +++ b/internal/install/install_controller.go @@ -163,7 +163,7 @@ func InitBaseInfo(ctx *gin.Context) { } if cli.CheckDBTableExist(c.Data.Database) { - log.Warnf("database is already initialized") + log.Warn("database is already initialized") handler.HandleResponse(ctx, nil, nil) return } diff --git a/internal/migrations/init.go b/internal/migrations/init.go index 93662e10..9bcfd994 100644 --- a/internal/migrations/init.go +++ b/internal/migrations/init.go @@ -246,6 +246,26 @@ func initConfigTable(engine *xorm.Engine) error { {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`}, } _, err := engine.Insert(defaultConfigTable) return err diff --git a/internal/migrations/migrations.go b/internal/migrations/migrations.go index da34800c..0e74d1d4 100644 --- a/internal/migrations/migrations.go +++ b/internal/migrations/migrations.go @@ -44,6 +44,7 @@ var migrations = []Migration{ NewMigration("this is first version, no operation", noopMigration), NewMigration("add user language", addUserLanguage), NewMigration("add recommend and reserved tag fields", addTagRecommendedAndReserved), + NewMigration("add activity timeline", addActivityTimeline), } // GetCurrentDBVersion returns the current db version diff --git a/internal/migrations/v3.go b/internal/migrations/v3.go new file mode 100644 index 00000000..d7e68f10 --- /dev/null +++ b/internal/migrations/v3.go @@ -0,0 +1,19 @@ +package migrations + +import ( + "time" + + "xorm.io/xorm" +) + +func addActivityTimeline(x *xorm.Engine) error { + type Revision struct { + ReviewUserID int64 `xorm:"not null default 0 BIGINT(20) review_user_id"` + } + type Activity struct { + CancelledAt time.Time `xorm:"TIMESTAMP cancelled_at"` + RevisionID int64 `xorm:"not null default 0 BIGINT(20) revision_id"` + OriginalObjectID string `xorm:"not null default 0 BIGINT(20) original_object_id"` + } + return x.Sync(new(Activity), new(Revision)) +} diff --git a/internal/repo/activity/activity_repo.go b/internal/repo/activity/activity_repo.go new file mode 100644 index 00000000..e192bd16 --- /dev/null +++ b/internal/repo/activity/activity_repo.go @@ -0,0 +1,35 @@ +package activity + +import ( + "context" + + "github.com/answerdev/answer/internal/base/data" + "github.com/answerdev/answer/internal/base/reason" + "github.com/answerdev/answer/internal/entity" + "github.com/answerdev/answer/internal/service/activity" + "github.com/segmentfault/pacman/errors" +) + +// activityRepo activity repository +type activityRepo struct { + data *data.Data +} + +// NewActivityRepo new repository +func NewActivityRepo( + data *data.Data, +) activity.ActivityRepo { + return &activityRepo{ + data: data, + } +} + +func (ar *activityRepo) GetObjectAllActivity(ctx context.Context, objectID string, showVote bool) ( + activityList []*entity.Activity, err error) { + activityList = make([]*entity.Activity, 0) + err = ar.data.DB.Find(&activityList, &entity.Activity{OriginalObjectID: objectID}) + if err != nil { + return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return activityList, nil +} diff --git a/internal/repo/activity/answer_repo.go b/internal/repo/activity/answer_repo.go index 01602b1e..1c47c8e1 100644 --- a/internal/repo/activity/answer_repo.go +++ b/internal/repo/activity/answer_repo.go @@ -2,6 +2,7 @@ package activity import ( "context" + "time" "github.com/answerdev/answer/internal/base/constant" "github.com/answerdev/answer/internal/base/data" @@ -96,8 +97,8 @@ func (ar *AnswerActivityRepo) DeleteQuestion(ctx context.Context, questionID str return nil, errors.InternalServer(reason.DatabaseError).WithError(e).WithStack() } - if _, e := session.Where("id = ?", act.ID).Cols("`cancelled`"). - Update(&entity.Activity{Cancelled: entity.ActivityCancelled}); e != nil { + 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() } } @@ -124,7 +125,7 @@ func (ar *AnswerActivityRepo) DeleteQuestion(ctx context.Context, questionID str // AcceptAnswer accept other answer func (ar *AnswerActivityRepo) AcceptAnswer(ctx context.Context, - answerObjID, questionUserID, answerUserID string, isSelf bool, + answerObjID, questionObjID, questionUserID, answerUserID string, isSelf bool, ) (err error) { addActivityList := make([]*entity.Activity, 0) for _, action := range acceptActionList { @@ -134,10 +135,11 @@ func (ar *AnswerActivityRepo) AcceptAnswer(ctx context.Context, return errors.InternalServer(reason.DatabaseError).WithError(e).WithStack() } addActivity := &entity.Activity{ - ObjectID: answerObjID, - ActivityType: activityType, - Rank: deltaRank, - HasRank: hasRank, + ObjectID: answerObjID, + OriginalObjectID: questionObjID, + ActivityType: activityType, + Rank: deltaRank, + HasRank: hasRank, } if action == acceptAction { addActivity.UserID = questionUserID @@ -222,7 +224,7 @@ func (ar *AnswerActivityRepo) AcceptAnswer(ctx context.Context, // CancelAcceptAnswer accept other answer func (ar *AnswerActivityRepo) CancelAcceptAnswer(ctx context.Context, - answerObjID, questionUserID, answerUserID string, + answerObjID, questionObjID, questionUserID, answerUserID string, ) (err error) { addActivityList := make([]*entity.Activity, 0) for _, action := range acceptActionList { @@ -232,10 +234,11 @@ func (ar *AnswerActivityRepo) CancelAcceptAnswer(ctx context.Context, return errors.InternalServer(reason.DatabaseError).WithError(e).WithStack() } addActivity := &entity.Activity{ - ObjectID: answerObjID, - ActivityType: activityType, - Rank: -deltaRank, - HasRank: hasRank, + ObjectID: answerObjID, + OriginalObjectID: questionObjID, + ActivityType: activityType, + Rank: -deltaRank, + HasRank: hasRank, } if action == acceptAction { addActivity.UserID = questionUserID @@ -265,8 +268,8 @@ func (ar *AnswerActivityRepo) CancelAcceptAnswer(ctx context.Context, return nil, errors.InternalServer(reason.DatabaseError).WithError(e).WithStack() } - if _, e := session.Where("id = ?", existsActivity.ID).Cols("`cancelled`"). - Update(&entity.Activity{Cancelled: entity.ActivityCancelled}); e != nil { + 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() } } @@ -326,8 +329,8 @@ func (ar *AnswerActivityRepo) DeleteAnswer(ctx context.Context, answerID string) return nil, errors.InternalServer(reason.DatabaseError).WithError(e).WithStack() } - if _, e := session.Where("id = ?", act.ID).Cols("`cancelled`"). - Update(&entity.Activity{Cancelled: entity.ActivityCancelled}); e != nil { + 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() } } diff --git a/internal/repo/activity/follow_repo.go b/internal/repo/activity/follow_repo.go index 14238718..bb1a0a0c 100644 --- a/internal/repo/activity/follow_repo.go +++ b/internal/repo/activity/follow_repo.go @@ -2,6 +2,7 @@ package activity import ( "context" + "time" "github.com/answerdev/answer/internal/service/activity_common" "github.com/answerdev/answer/internal/service/follow" @@ -59,7 +60,7 @@ func (ar *FollowRepo) Follow(ctx context.Context, objectID, userID string) error return } - if has && existsActivity.Cancelled == 0 { + if has && existsActivity.Cancelled == entity.ActivityAvailable { return } @@ -67,17 +68,18 @@ func (ar *FollowRepo) Follow(ctx context.Context, objectID, userID string) error _, err = session.Where(builder.Eq{"id": existsActivity.ID}). Cols(`cancelled`). Update(&entity.Activity{ - Cancelled: 0, + Cancelled: entity.ActivityAvailable, }) } else { // update existing activity with new user id and u object id _, err = session.Insert(&entity.Activity{ - UserID: userID, - ObjectID: objectID, - ActivityType: activityType, - Cancelled: 0, - Rank: 0, - HasRank: 0, + UserID: userID, + ObjectID: objectID, + OriginalObjectID: objectID, + ActivityType: activityType, + Cancelled: entity.ActivityAvailable, + Rank: 0, + HasRank: 0, }) } @@ -120,13 +122,14 @@ func (ar *FollowRepo) FollowCancel(ctx context.Context, objectID, userID string) return } - if has && existsActivity.Cancelled == 1 { + if has && existsActivity.Cancelled == entity.ActivityCancelled { return } if _, err = session.Where("id = ?", existsActivity.ID). Cols("cancelled"). Update(&entity.Activity{ - Cancelled: 1, + Cancelled: entity.ActivityCancelled, + CancelledAt: time.Now(), }); err != nil { return } diff --git a/internal/repo/activity/user_active_repo.go b/internal/repo/activity/user_active_repo.go index 27f6c5a6..1b75adac 100644 --- a/internal/repo/activity/user_active_repo.go +++ b/internal/repo/activity/user_active_repo.go @@ -55,11 +55,12 @@ func (ar *UserActiveActivityRepo) UserActive(ctx context.Context, userID string) } addActivity := &entity.Activity{ - UserID: userID, - ObjectID: "0", - ActivityType: activityType, - Rank: deltaRank, - HasRank: 1, + UserID: userID, + ObjectID: "0", + OriginalObjectID: "0", + ActivityType: activityType, + Rank: deltaRank, + HasRank: 1, } _, exists, err := ar.activityRepo.GetActivity(ctx, session, "0", addActivity.UserID, activityType) if err != nil { diff --git a/internal/repo/activity/vote_repo.go b/internal/repo/activity/vote_repo.go index 00ccd04b..f934329f 100644 --- a/internal/repo/activity/vote_repo.go +++ b/internal/repo/activity/vote_repo.go @@ -3,6 +3,7 @@ package activity import ( "context" "strings" + "time" "github.com/answerdev/answer/pkg/converter" @@ -100,18 +101,19 @@ func (vr *VoteRepo) vote(ctx context.Context, objectID string, userID, objectUse Get(&existsActivity) // is is voted,return - if has && existsActivity.Cancelled == 0 { + if has && existsActivity.Cancelled == entity.ActivityAvailable { return } insertActivity = entity.Activity{ - ObjectID: objectID, - UserID: activityUserID, - TriggerUserID: converter.StringToInt64(triggerUserID), - ActivityType: activityType, - Rank: deltaRank, - HasRank: hasRank, - Cancelled: 0, + 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 @@ -131,7 +133,7 @@ func (vr *VoteRepo) vote(ctx context.Context, objectID string, userID, objectUse if has { if _, err = session.Where("id = ?", existsActivity.ID).Cols("`cancelled`"). Update(&entity.Activity{ - Cancelled: 0, + Cancelled: entity.ActivityAvailable, }); err != nil { return } @@ -201,13 +203,14 @@ func (vr *VoteRepo) voteCancel(ctx context.Context, objectID string, userID, obj return } - if existsActivity.Cancelled == 1 { + if existsActivity.Cancelled == entity.ActivityCancelled { return } - if _, err = session.Where("id = ?", existsActivity.ID).Cols("`cancelled`"). + if _, err = session.Where("id = ?", existsActivity.ID).Cols("cancelled", "cancelled_at"). Update(&entity.Activity{ - Cancelled: 1, + Cancelled: entity.ActivityCancelled, + CancelledAt: time.Now(), }); err != nil { return } diff --git a/internal/repo/activity_common/activity_repo.go b/internal/repo/activity_common/activity_repo.go index d52db3f2..efb7d3be 100644 --- a/internal/repo/activity_common/activity_repo.go +++ b/internal/repo/activity_common/activity_repo.go @@ -63,6 +63,14 @@ func (ar *ActivityRepo) GetActivityTypeByObjKey(ctx context.Context, objectKey, return } +func (ar *ActivityRepo) GetActivityTypeByConfigKey(ctx context.Context, configKey string) (activityType int, err error) { + activityType, err = ar.configRepo.GetConfigType(configKey) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return +} + func (ar *ActivityRepo) GetActivity(ctx context.Context, session *xorm.Session, objectID, userID string, activityType int, ) (existsActivity *entity.Activity, exist bool, err error) { @@ -89,3 +97,12 @@ func (ar *ActivityRepo) GetUserIDObjectIDActivitySum(ctx context.Context, userID } return sum.Rank, nil } + +// AddActivity add activity +func (ar *ActivityRepo) AddActivity(ctx context.Context, activity *entity.Activity) (err error) { + _, err = ar.data.DB.Insert(activity) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return +} diff --git a/internal/repo/activity_common/follow.go b/internal/repo/activity_common/follow.go index aee42454..5f7b0ec0 100644 --- a/internal/repo/activity_common/follow.go +++ b/internal/repo/activity_common/follow.go @@ -124,7 +124,7 @@ func (ar *FollowRepo) IsFollowed(userID, objectID string) (bool, error) { if !has { return false, nil } - if at.Cancelled == 1 { + if at.Cancelled == entity.ActivityCancelled { return false, nil } else { return true, nil diff --git a/internal/repo/provider.go b/internal/repo/provider.go index 3f1ee7c2..7bc82aee 100644 --- a/internal/repo/provider.go +++ b/internal/repo/provider.go @@ -54,6 +54,7 @@ var ProviderSetRepo = wire.NewSet( activity.NewAnswerActivityRepo, activity.NewQuestionActivityRepo, activity.NewUserActiveActivityRepo, + activity.NewActivityRepo, tag.NewTagRepo, tag_common.NewTagCommonRepo, tag.NewTagRelRepo, diff --git a/internal/repo/question/question_repo.go b/internal/repo/question/question_repo.go index 4d90a09e..18da5594 100644 --- a/internal/repo/question/question_repo.go +++ b/internal/repo/question/question_repo.go @@ -166,7 +166,7 @@ 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.In("question.status", []int{entity.QuestionStatusAvailable, entity.QuestionStatusclosed}).FindAndCount(&questionList) + count, err = qr.data.DB.In("question.status", []int{entity.QuestionStatusAvailable, entity.QuestionStatusClosed}).FindAndCount(&questionList) if err != nil { return count, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } @@ -210,7 +210,7 @@ func (qr *questionRepo) SearchList(ctx context.Context, search *schema.QuestionS session = session.And("question.user_id = ?", search.UserID) } - session = session.In("question.status", []int{entity.QuestionStatusAvailable, entity.QuestionStatusclosed}) + session = session.In("question.status", []int{entity.QuestionStatusAvailable, entity.QuestionStatusClosed}) // if search.Status > 0 { // session = session.And("question.status = ?", search.Status) // } diff --git a/internal/repo/revision/revision_repo.go b/internal/repo/revision/revision_repo.go index 0d1c474a..d72a5bb8 100644 --- a/internal/repo/revision/revision_repo.go +++ b/internal/repo/revision/revision_repo.go @@ -5,6 +5,7 @@ import ( "github.com/answerdev/answer/internal/base/constant" "github.com/answerdev/answer/internal/base/data" + "github.com/answerdev/answer/internal/base/pager" "github.com/answerdev/answer/internal/base/reason" "github.com/answerdev/answer/internal/entity" "github.com/answerdev/answer/internal/service/revision" @@ -79,6 +80,21 @@ func (rr *revisionRepo) UpdateObjectRevisionId(ctx context.Context, revision *en return nil } +// UpdateStatus update revision status +func (rr *revisionRepo) UpdateStatus(ctx context.Context, id string, status int) (err error) { + if id == "" { + return nil + } + var data entity.Revision + data.ID = id + data.Status = status + _, err = rr.data.DB.Where("id =?", id).Cols("status").Update(&data) + if err != nil { + return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return nil +} + // GetRevision get revision one func (rr *revisionRepo) GetRevision(ctx context.Context, id string) ( revision *entity.Revision, exist bool, err error, @@ -91,6 +107,27 @@ func (rr *revisionRepo) GetRevision(ctx context.Context, id string) ( return } +// GetRevisionByID get object's last revision by object TagID +func (rr *revisionRepo) GetRevisionByID(ctx context.Context, revisionID string) ( + revision *entity.Revision, exist bool, err error) { + revision = &entity.Revision{} + exist, err = rr.data.DB.Where("id = ?", revisionID).Get(revision) + if err != nil { + return nil, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return +} + +func (rr *revisionRepo) ExistUnreviewedByObjectID(ctx context.Context, objectID string) ( + revision *entity.Revision, exist bool, err error) { + revision = &entity.Revision{} + exist, err = rr.data.DB.Where("object_id = ?", objectID).And("status = ?", entity.RevisionUnreviewedStatus).Get(revision) + if err != nil { + return nil, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return +} + // GetLastRevisionByObjectID get object's last revision by object TagID func (rr *revisionRepo) GetLastRevisionByObjectID(ctx context.Context, objectID string) ( revision *entity.Revision, exist bool, err error, @@ -128,3 +165,22 @@ func (rr *revisionRepo) allowRecord(objectType int) (ok bool) { return false } } + +// GetUnreviewedRevisionPage get unreviewed revision page +func (rr *revisionRepo) GetUnreviewedRevisionPage(ctx context.Context, page int, pageSize int, + objectTypeList []int) (revisionList []*entity.Revision, total int64, err error) { + revisionList = make([]*entity.Revision, 0) + if len(objectTypeList) == 0 { + return revisionList, 0, nil + } + session := rr.data.DB.NewSession() + session = session.And("status = ?", entity.RevisionUnreviewedStatus) + session = session.In("object_type", objectTypeList) + session = session.OrderBy("created_at desc") + + total, err = pager.Help(page, pageSize, &revisionList, &entity.Revision{}, session) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return +} diff --git a/internal/repo/search_common/search_repo.go b/internal/repo/search_common/search_repo.go index f1061300..aec7dc59 100644 --- a/internal/repo/search_common/search_repo.go +++ b/internal/repo/search_common/search_repo.go @@ -471,20 +471,6 @@ func (sr *searchRepo) parseResult(ctx context.Context, res []map[string][]byte) return } -// userBasicInfoFormat -func (sr *searchRepo) userBasicInfoFormat(ctx context.Context, dbinfo *entity.User) *schema.UserBasicInfo { - return &schema.UserBasicInfo{ - ID: dbinfo.ID, - Username: dbinfo.Username, - Rank: dbinfo.Rank, - DisplayName: dbinfo.DisplayName, - Avatar: dbinfo.Avatar, - Website: dbinfo.Website, - Location: dbinfo.Location, - IPInfo: dbinfo.IPInfo, - } -} - func addRelevanceField(searchFields, words, fields []string) (res []string, args []interface{}) { relevanceRes := []string{} args = []interface{}{} diff --git a/internal/repo/tag/tag_repo.go b/internal/repo/tag/tag_repo.go index 4a9222e3..0c1169e0 100644 --- a/internal/repo/tag/tag_repo.go +++ b/internal/repo/tag/tag_repo.go @@ -6,7 +6,7 @@ import ( "github.com/answerdev/answer/internal/base/data" "github.com/answerdev/answer/internal/base/reason" "github.com/answerdev/answer/internal/entity" - "github.com/answerdev/answer/internal/service/tag" + "github.com/answerdev/answer/internal/service/tag_common" "github.com/answerdev/answer/internal/service/unique" "github.com/segmentfault/pacman/errors" "xorm.io/builder" @@ -22,7 +22,7 @@ type tagRepo struct { func NewTagRepo( data *data.Data, uniqueIDRepo unique.UniqueIDRepo, -) tag.TagRepo { +) tag_common.TagRepo { return &tagRepo{ data: data, uniqueIDRepo: uniqueIDRepo, diff --git a/internal/repo/user/user_repo.go b/internal/repo/user/user_repo.go index fb93c53c..10324baa 100644 --- a/internal/repo/user/user_repo.go +++ b/internal/repo/user/user_repo.go @@ -28,7 +28,7 @@ func NewUserRepo(data *data.Data, configRepo config.ConfigRepo) usercommon.UserR // AddUser add user func (ur *userRepo) AddUser(ctx context.Context, user *entity.User) (err error) { - _, err = ur.data.DB.Insert(user) + _, err = ur.data.DB.UseBool("is_admin").Insert(user) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } diff --git a/internal/router/answer_api_router.go b/internal/router/answer_api_router.go index 1920d619..5a61df29 100644 --- a/internal/router/answer_api_router.go +++ b/internal/router/answer_api_router.go @@ -29,6 +29,7 @@ type AnswerAPIRouter struct { notificationController *controller.NotificationController dashboardController *controller.DashboardController uploadController *controller.UploadController + activityController *controller.ActivityController roleController *controller_backyard.RoleController } @@ -55,6 +56,7 @@ func NewAnswerAPIRouter( notificationController *controller.NotificationController, dashboardController *controller.DashboardController, uploadController *controller.UploadController, + activityController *controller.ActivityController, roleController *controller_backyard.RoleController, ) *AnswerAPIRouter { return &AnswerAPIRouter{ @@ -80,6 +82,7 @@ func NewAnswerAPIRouter( siteinfoController: siteinfoController, dashboardController: dashboardController, uploadController: uploadController, + activityController: activityController, roleController: roleController, } } @@ -140,9 +143,15 @@ func (a *AnswerAPIRouter) RegisterUnAuthAnswerAPIRouter(r *gin.RouterGroup) { //siteinfo r.GET("/siteinfo", a.siteinfoController.GetSiteInfo) r.GET("/siteinfo/legal", a.siteinfoController.GetSiteLegalInfo) + } func (a *AnswerAPIRouter) RegisterAnswerAPIRouter(r *gin.RouterGroup) { + //revisions + r.GET("/revisions/unreviewed", a.revisionController.GetUnreviewedRevisionList) + r.PUT("/revisions/audit", a.revisionController.RevisionAudit) + r.GET("/revisions/edit/check", a.revisionController.CheckCanUpdateRevision) + // comment r.POST("/comment", a.commentController.AddComment) r.DELETE("/comment", a.commentController.RemoveComment) @@ -203,6 +212,11 @@ func (a *AnswerAPIRouter) RegisterAnswerAPIRouter(r *gin.RouterGroup) { // upload file r.POST("/file", a.uploadController.UploadFile) + + // activity + r.GET("/activity/timeline", a.activityController.GetObjectTimeline) + r.GET("/activity/timeline/detail", a.activityController.GetObjectTimelineDetail) + } func (a *AnswerAPIRouter) RegisterAnswerCmsAPIRouter(r *gin.RouterGroup) { diff --git a/internal/schema/activity.go b/internal/schema/activity.go new file mode 100644 index 00000000..be90a845 --- /dev/null +++ b/internal/schema/activity.go @@ -0,0 +1,71 @@ +package schema + +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"` +} + +// GetObjectTimelineReq get object timeline request +type GetObjectTimelineReq struct { + ObjectID string `validate:"omitempty,gt=0,lte=100" form:"object_id"` + ShowVote bool `validate:"omitempty" form:"show_vote"` + UserID string `json:"-"` +} + +// GetObjectTimelineResp get object timeline response +type GetObjectTimelineResp struct { + ObjectInfo *ActObjectInfo `json:"object_info"` + Timeline []*ActObjectTimeline `json:"timeline"` +} + +// ActObjectTimeline act object timeline +type ActObjectTimeline struct { + ActivityID string `json:"activity_id"` + RevisionID string `json:"revision_id"` + CreatedAt int64 `json:"created_at"` + ActivityType string `json:"activity_type"` + Username string `json:"username"` + UserDisplayName string `json:"user_display_name"` + Comment string `json:"comment"` + ObjectID string `json:"object_id"` + ObjectType string `json:"object_type"` + Cancelled bool `json:"cancelled"` + CancelledAt int64 `json:"cancelled_at"` +} + +// ActObjectInfo act object info +type ActObjectInfo struct { + Title string `json:"title"` + ObjectType string `json:"object_type"` + QuestionID string `json:"question_id"` + AnswerID string `json:"answer_id"` + Username string `json:"username"` + DisplayName string `json:"display_name"` +} + +// GetObjectTimelineDetailReq get object timeline detail request +type GetObjectTimelineDetailReq struct { + NewRevisionID string `validate:"required,gt=0,lte=100" form:"new_revision_id"` + OldRevisionID string `validate:"required,gt=0,lte=100" form:"old_revision_id"` + UserID string `json:"-"` +} + +// GetObjectTimelineDetailResp get object timeline detail response +type GetObjectTimelineDetailResp struct { + NewRevision *ObjectTimelineDetail `json:"new_revision"` + OldRevision *ObjectTimelineDetail `json:"old_revision"` +} + +// ObjectTimelineDetail object timeline detail +type ObjectTimelineDetail struct { + Title string `json:"title"` + Tags []string `json:"tags"` + OriginalText string `json:"original_text"` +} diff --git a/internal/schema/answer_schema.go b/internal/schema/answer_schema.go index 0c396865..0a88eefa 100644 --- a/internal/schema/answer_schema.go +++ b/internal/schema/answer_schema.go @@ -5,7 +5,8 @@ type RemoveAnswerReq struct { // answer id ID string `validate:"required" json:"id"` // user id - UserID string `json:"-"` + UserID string `json:"-"` + IsAdmin bool `json:"-"` } const ( @@ -21,21 +22,34 @@ type AnswerAddReq struct { } type AnswerUpdateReq struct { - ID string `json:"id"` // id - QuestionID string `json:"question_id" ` // question_id - UserID string `json:"-" ` // user_id - Title string `json:"title" ` // title - Content string `json:"content"` // content - HTML string `json:"html" ` // html - EditSummary string `validate:"omitempty" json:"edit_summary"` // edit_summary + ID string `json:"id"` // id + QuestionID string `json:"question_id" ` // question_id + UserID string `json:"-" ` // user_id + Title string `json:"title" ` // title + Content string `json:"content"` // content + HTML string `json:"html" ` // html + EditSummary string `validate:"omitempty" json:"edit_summary"` // edit_summary + NoNeedReview bool `json:"-"` + // whether user can edit it + CanEdit bool `json:"-"` } -type AnswerList struct { - QuestionID string `json:"question_id" form:"question_id"` // question_id - Order string `json:"order" form:"order"` // 1 Default 2 time - Page int `json:"page" form:"page"` // Query number of pages - PageSize int `json:"page_size" form:"page_size"` // Search page size - LoginUserID string `json:"-" ` +// AnswerUpdateResp answer update resp +type AnswerUpdateResp struct { + WaitForReview bool `json:"wait_for_review"` +} + +type AnswerListReq struct { + QuestionID string `json:"question_id" form:"question_id"` // question_id + Order string `json:"order" form:"order"` // 1 Default 2 time + Page int `json:"page" form:"page"` // Query number of pages + PageSize int `json:"page_size" form:"page_size"` // Search page size + UserID string `json:"-" ` + IsAdmin bool `json:"-"` + // whether user can edit it + CanEdit bool `json:"-"` + // whether user can delete it + CanDelete bool `json:"-"` } type AnswerInfo struct { @@ -47,6 +61,7 @@ type AnswerInfo struct { UpdateTime int64 `json:"update_time" xorm:"updated"` // update_time Adopted int `json:"adopted"` // 1 Failed 2 Adopted UserID string `json:"-" ` + UpdateUserID string `json:"-" ` UserInfo *UserBasicInfo `json:"user_info,omitempty"` UpdateUserInfo *UserBasicInfo `json:"update_user_info,omitempty"` Collected bool `json:"collected"` @@ -66,6 +81,7 @@ type AdminAnswerInfo struct { UpdateTime int64 `json:"update_time"` Adopted int `json:"adopted"` UserID string `json:"-" ` + UpdateUserID string `json:"-" ` UserInfo *UserBasicInfo `json:"user_info"` VoteCount int `json:"vote_count"` QuestionInfo struct { diff --git a/internal/schema/comment_schema.go b/internal/schema/comment_schema.go index 53ebf445..84c93d08 100644 --- a/internal/schema/comment_schema.go +++ b/internal/schema/comment_schema.go @@ -19,6 +19,12 @@ type AddCommentReq struct { MentionUsernameList []string `validate:"omitempty" json:"mention_username_list"` // user id UserID string `json:"-"` + // whether user can add it + CanAdd bool `json:"-"` + // whether user can edit it + CanEdit bool `json:"-"` + // whether user can delete it + CanDelete bool `json:"-"` } // RemoveCommentReq remove comment @@ -73,6 +79,10 @@ type GetCommentWithPageReq struct { QueryCond string `validate:"omitempty,oneof=vote" form:"query_cond"` // user id UserID string `json:"-"` + // whether user can edit it + CanEdit bool `json:"-"` + // whether user can delete it + CanDelete bool `json:"-"` } // GetCommentReq get comment list page request @@ -81,6 +91,10 @@ type GetCommentReq struct { ID string `validate:"required" form:"id"` // user id UserID string `json:"-"` + // whether user can edit it + CanEdit bool `json:"-"` + // whether user can delete it + CanDelete bool `json:"-"` } // GetCommentResp comment response diff --git a/internal/schema/notification_schema.go b/internal/schema/notification_schema.go index 6a8f470f..8688af37 100644 --- a/internal/schema/notification_schema.go +++ b/internal/schema/notification_schema.go @@ -27,6 +27,13 @@ type NotificationContent struct { UpdateTime int64 `json:"update_time"` } +type GetRedDot struct { + CanReviewQuestion bool `json:"-"` + CanReviewAnswer bool `json:"-"` + CanReviewTag bool `json:"-"` + UserID string `json:"-"` +} + // NotificationMsg notification message type NotificationMsg struct { // trigger notification user id @@ -57,6 +64,8 @@ type ObjectInfo struct { type RedDot struct { Inbox int64 `json:"inbox"` Achievement int64 `json:"achievement"` + Revision int64 `json:"revision"` + CanRevision bool `json:"can_revision"` } type NotificationSearch struct { @@ -68,8 +77,11 @@ type NotificationSearch struct { } type NotificationClearRequest struct { - UserID string `json:"-"` - TypeStr string `json:"type" form:"type"` // inbox achievement + UserID string `json:"-"` + TypeStr string `json:"type" form:"type"` // inbox achievement + CanReviewQuestion bool `json:"-"` + CanReviewAnswer bool `json:"-"` + CanReviewTag bool `json:"-"` } type NotificationClearIDRequest struct { diff --git a/internal/schema/question_schema.go b/internal/schema/question_schema.go index 29b85be8..f9be0293 100644 --- a/internal/schema/question_schema.go +++ b/internal/schema/question_schema.go @@ -3,8 +3,9 @@ package schema // RemoveQuestionReq delete question request type RemoveQuestionReq struct { // question id - ID string `validate:"required" comment:"question id" json:"id"` - UserID string `json:"-" ` // user_id + ID string `validate:"required" comment:"question id" json:"id"` + UserID string `json:"-" ` // user_id + IsAdmin bool `json:"-"` } type CloseQuestionReq struct { @@ -12,6 +13,7 @@ type CloseQuestionReq struct { UserID string `json:"-" ` // user_id CloseType int `json:"close_type" ` // close_type CloseMsg string `json:"close_msg" ` // close_type + IsAdmin bool `json:"-"` } type CloseQuestionMeta struct { @@ -30,6 +32,26 @@ type QuestionAdd struct { Tags []*TagItem `validate:"required,dive" json:"tags"` // user id UserID string `json:"-"` + QuestionPermission +} + +type QuestionPermission struct { + // whether user can add it + CanAdd bool `json:"-"` + // whether user can edit it + CanEdit bool `json:"-"` + // whether user can delete it + CanDelete bool `json:"-"` + // whether user can close it + CanClose bool `json:"-"` +} + +type CheckCanQuestionUpdate struct { + // question id + ID string `validate:"required" form:"id"` + // user id + UserID string `json:"-"` + IsAdmin bool `json:"-"` } type QuestionUpdate struct { @@ -46,7 +68,10 @@ type QuestionUpdate struct { // edit summary EditSummary string `validate:"omitempty" json:"edit_summary"` // user id - UserID string `json:"-"` + UserID string `json:"-"` + IsAdmin bool `json:"-"` + NoNeedReview bool `json:"-"` + QuestionPermission } type QuestionBaseInfo struct { @@ -81,6 +106,8 @@ type QuestionInfo struct { Status int `json:"status"` Operation *Operation `json:"operation,omitempty"` UserID string `json:"-" ` + LastEditUserID string `json:"-" ` + LastAnsweredUserID string `json:"-" ` UserInfo *UserBasicInfo `json:"user_info"` UpdateUserInfo *UserBasicInfo `json:"update_user_info,omitempty"` LastAnsweredUserInfo *UserBasicInfo `json:"last_answered_user_info,omitempty"` @@ -93,6 +120,11 @@ type QuestionInfo struct { MemberActions []*PermissionMemberAction `json:"member_actions"` } +// UpdateQuestionResp update question resp +type UpdateQuestionResp struct { + WaitForReview bool `json:"wait_for_review"` +} + type AdminQuestionInfo struct { ID string `json:"id"` Title string `json:"title"` @@ -169,7 +201,7 @@ type CmsQuestionSearch 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 + 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 } diff --git a/internal/schema/revision_schema.go b/internal/schema/revision_schema.go index 826b3030..becb7935 100644 --- a/internal/schema/revision_schema.go +++ b/internal/schema/revision_schema.go @@ -2,6 +2,8 @@ package schema import ( "time" + + "github.com/answerdev/answer/internal/base/constant" ) // AddRevisionDTO add revision request @@ -16,6 +18,8 @@ type AddRevisionDTO struct { Content string // log Log string + // status + Status int } // GetRevisionListReq get revision list all request @@ -24,6 +28,47 @@ type GetRevisionListReq struct { ObjectID string `validate:"required" comment:"object_id" form:"object_id"` } +const RevisionAuditApprove = "approve" +const RevisionAuditReject = "reject" + +type RevisionAuditReq struct { + // object id + ID string `validate:"required" comment:"id" form:"id"` + Operation string `validate:"required" comment:"operation" form:"operation"` //approve or reject + UserID string `json:"-"` + CanReviewQuestion bool `json:"-"` + CanReviewAnswer bool `json:"-"` + CanReviewTag bool `json:"-"` +} + +type RevisionSearch struct { + Page int `json:"page" form:"page"` // Query number of pages + CanReviewQuestion bool `json:"-"` + CanReviewAnswer bool `json:"-"` + CanReviewTag bool `json:"-"` + UserID string `json:"-"` +} + +func (r RevisionSearch) GetCanReviewObjectTypes() []int { + objectType := make([]int, 0) + if r.CanReviewAnswer { + objectType = append(objectType, constant.ObjectTypeStrMapping[constant.AnswerObjectType]) + } + if r.CanReviewQuestion { + objectType = append(objectType, constant.ObjectTypeStrMapping[constant.QuestionObjectType]) + } + if r.CanReviewTag { + objectType = append(objectType, constant.ObjectTypeStrMapping[constant.TagObjectType]) + } + return objectType +} + +type GetUnreviewedRevisionResp struct { + Type string `json:"type"` + Info *UnreviewedRevisionInfoInfo `json:"info"` + UnreviewedInfo *GetRevisionResp `json:"unreviewed_info"` +} + // GetRevisionResp get revision response type GetRevisionResp struct { // id diff --git a/internal/schema/simple_obj_info_schema.go b/internal/schema/simple_obj_info_schema.go index 312772db..28a7b775 100644 --- a/internal/schema/simple_obj_info_schema.go +++ b/internal/schema/simple_obj_info_schema.go @@ -2,13 +2,21 @@ package schema // SimpleObjectInfo simple object info type SimpleObjectInfo struct { - ObjectID string `json:"object_id"` - ObjectCreator string `json:"object_creator"` - QuestionID string `json:"question_id"` - AnswerID string `json:"answer_id"` - CommentID string `json:"comment_id"` - TagID string `json:"tag_id"` - ObjectType string `json:"object_type"` - Title string `json:"title"` - Content string `json:"content"` + ObjectID string `json:"object_id"` + ObjectCreatorUserID string `json:"object_creator_user_id"` + QuestionID string `json:"question_id"` + AnswerID string `json:"answer_id"` + CommentID string `json:"comment_id"` + TagID string `json:"tag_id"` + ObjectType string `json:"object_type"` + Title string `json:"title"` + Content string `json:"content"` +} + +type UnreviewedRevisionInfoInfo struct { + ObjectID string `json:"object_id"` + Title string `json:"title"` + Content string `json:"content"` + Html string `json:"html"` + Tags []*TagResp `json:"tags"` } diff --git a/internal/schema/siteinfo_schema.go b/internal/schema/siteinfo_schema.go index 88c767e9..d1292623 100644 --- a/internal/schema/siteinfo_schema.go +++ b/internal/schema/siteinfo_schema.go @@ -8,8 +8,8 @@ import ( // SiteGeneralReq site general request type SiteGeneralReq struct { Name string `validate:"required,gt=1,lte=128" form:"name" json:"name"` - ShortDescription string `validate:"required,gt=3,lte=255" form:"short_description" json:"short_description"` - Description string `validate:"required,gt=3,lte=2000" form:"description" json:"description"` + ShortDescription string `validate:"omitempty,gt=3,lte=255" form:"short_description" json:"short_description"` + Description string `validate:"omitempty,gt=3,lte=2000" form:"description" json:"description"` SiteUrl string `validate:"required,gt=1,lte=512,url" form:"site_url" json:"site_url"` ContactEmail string `validate:"required,gt=1,lte=512,email" form:"contact_email" json:"contact_email"` } diff --git a/internal/schema/tag_schema.go b/internal/schema/tag_schema.go index 8e1b6da0..bf79d5aa 100644 --- a/internal/schema/tag_schema.go +++ b/internal/schema/tag_schema.go @@ -23,6 +23,10 @@ type GetTagInfoReq struct { Name string `validate:"omitempty,gt=0,lte=35" form:"name"` // user id UserID string `json:"-"` + // whether user can edit it + CanEdit bool `json:"-"` + // whether user can delete it + CanDelete bool `json:"-"` } func (r *GetTagInfoReq) Check() (errFields []*validator.FormErrorField, err error) { @@ -152,7 +156,8 @@ type UpdateTagReq struct { // edit summary EditSummary string `validate:"omitempty" json:"edit_summary"` // user id - UserID string `json:"-"` + UserID string `json:"-"` + NoNeedReview bool `json:"-"` } func (r *UpdateTagReq) Check() (errFields []*validator.FormErrorField, err error) { @@ -162,6 +167,11 @@ func (r *UpdateTagReq) Check() (errFields []*validator.FormErrorField, err error return nil, nil } +// UpdateTagResp update tag response +type UpdateTagResp struct { + WaitForReview bool `json:"wait_for_review"` +} + // GetTagWithPageReq get tag list page request type GetTagWithPageReq struct { // page @@ -182,10 +192,21 @@ type GetTagWithPageReq struct { type GetTagSynonymsReq struct { // tag_id TagID string `validate:"required" form:"tag_id"` + // user id + UserID string `json:"-"` + // whether user can edit it + CanEdit bool `json:"-"` } // GetTagSynonymsResp get tag synonyms response type GetTagSynonymsResp struct { + // synonyms + Synonyms []*TagSynonym `json:"synonyms"` + // MemberActions + MemberActions []*PermissionMemberAction `json:"member_actions"` +} + +type TagSynonym struct { // tag id TagID string `json:"tag_id"` // slug name diff --git a/internal/schema/user_schema.go b/internal/schema/user_schema.go index fdae241c..dd36f8c2 100644 --- a/internal/schema/user_schema.go +++ b/internal/schema/user_schema.go @@ -368,7 +368,8 @@ type ActionRecordResp struct { } type UserBasicInfo struct { - ID string `json:"-" ` // user_id + ID string `json:"-"` // user_id + IsAdmin bool `json:"-"` Username string `json:"username" ` // name Rank int `json:"rank" ` // rank DisplayName string `json:"display_name"` // display_name diff --git a/internal/schema/vote_schema.go b/internal/schema/vote_schema.go index ec37ebc8..8a17666f 100644 --- a/internal/schema/vote_schema.go +++ b/internal/schema/vote_schema.go @@ -3,6 +3,7 @@ package schema type VoteReq struct { ObjectID string `validate:"required" form:"object_id" json:"object_id"` // id IsCancel bool `validate:"omitempty" form:"is_cancel" json:"is_cancel"` // is cancel + UserID string `json:"-"` } type VoteDTO struct { diff --git a/internal/service/activity/activity.go b/internal/service/activity/activity.go new file mode 100644 index 00000000..2ef56a9c --- /dev/null +++ b/internal/service/activity/activity.go @@ -0,0 +1,211 @@ +package activity + +import ( + "context" + "encoding/json" + "strings" + + "github.com/answerdev/answer/internal/base/constant" + "github.com/answerdev/answer/internal/entity" + "github.com/answerdev/answer/internal/repo/config" + "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/object_info" + "github.com/answerdev/answer/internal/service/revision_common" + "github.com/answerdev/answer/internal/service/tag_common" + usercommon "github.com/answerdev/answer/internal/service/user_common" + "github.com/answerdev/answer/pkg/converter" + "github.com/segmentfault/pacman/log" +) + +// ActivityRepo activity repository +type ActivityRepo interface { + GetObjectAllActivity(ctx context.Context, objectID string, showVote bool) (activityList []*entity.Activity, err error) +} + +// 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 +} + +// 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, + revisionService *revision_common.RevisionService, +) *ActivityService { + return &ActivityService{ + objectInfoService: objectInfoService, + activityRepo: activityRepo, + userCommon: userCommon, + activityCommonService: activityCommonService, + tagCommonService: tagCommonService, + commentCommonService: commentCommonService, + revisionService: revisionService, + } +} + +// GetObjectTimeline get object timeline +func (as *ActivityService) GetObjectTimeline(ctx context.Context, req *schema.GetObjectTimelineReq) ( + resp *schema.GetObjectTimelineResp, err error) { + resp = &schema.GetObjectTimelineResp{ + ObjectInfo: &schema.ActObjectInfo{}, + Timeline: make([]*schema.ActObjectTimeline, 0), + } + + objInfo, err := as.objectInfoService.GetInfo(ctx, req.ObjectID) + if err != nil { + return nil, err + } + resp.ObjectInfo.Title = objInfo.Title + resp.ObjectInfo.ObjectType = objInfo.ObjectType + resp.ObjectInfo.QuestionID = objInfo.QuestionID + resp.ObjectInfo.AnswerID = objInfo.AnswerID + if len(objInfo.ObjectCreatorUserID) > 0 { + // get object creator user info + userBasicInfo, exist, err := as.userCommon.GetUserBasicInfoByID(ctx, objInfo.ObjectCreatorUserID) + if err != nil { + return nil, err + } + if exist { + resp.ObjectInfo.Username = userBasicInfo.Username + resp.ObjectInfo.DisplayName = userBasicInfo.DisplayName + } + } + + activityList, err := as.activityRepo.GetObjectAllActivity(ctx, req.ObjectID, req.ShowVote) + if err != nil { + return nil, err + } + for _, act := range activityList { + item := &schema.ActObjectTimeline{ + ActivityID: act.ID, + RevisionID: converter.IntToString(act.RevisionID), + CreatedAt: act.CreatedAt.Unix(), + Cancelled: act.Cancelled == entity.ActivityCancelled, + ObjectID: act.ObjectID, + } + if item.Cancelled { + item.CancelledAt = act.CancelledAt.Unix() + } + + // database save activity type is number, change to activity type string is like "question.asked". + // so we need to cut the front part of '.' + item.ObjectType, item.ActivityType, _ = strings.Cut(config.ID2KeyMapping[act.ActivityType], ".") + + isHidden, formattedActivityType := formatActivity(item.ActivityType) + if isHidden { + continue + } + item.ActivityType = formattedActivityType + + // get user info + userBasicInfo, exist, err := as.userCommon.GetUserBasicInfoByID(ctx, act.UserID) + if err != nil { + return nil, err + } + if exist { + item.Username = userBasicInfo.Username + item.UserDisplayName = userBasicInfo.DisplayName + } + + if item.ObjectType == constant.CommentObjectType { + comment, err := as.commentCommonService.GetComment(ctx, item.ObjectID) + if err != nil { + log.Error(err) + } else { + item.Comment = comment.ParsedText + } + } + + resp.Timeline = append(resp.Timeline, item) + } + return +} + +// GetObjectTimelineDetail get object timeline +func (as *ActivityService) GetObjectTimelineDetail(ctx context.Context, req *schema.GetObjectTimelineDetailReq) ( + resp *schema.GetObjectTimelineDetailResp, err error) { + resp = &schema.GetObjectTimelineDetailResp{} + resp.OldRevision, err = as.getOneObjectDetail(ctx, req.OldRevisionID) + if err != nil { + return nil, err + } + resp.NewRevision, err = as.getOneObjectDetail(ctx, req.NewRevisionID) + if err != nil { + return nil, err + } + return resp, nil +} + +// GetObjectTimelineDetail get object detail +func (as *ActivityService) getOneObjectDetail(ctx context.Context, revisionID string) ( + resp *schema.ObjectTimelineDetail, err error) { + resp = &schema.ObjectTimelineDetail{Tags: make([]string, 0)} + + revision, err := as.revisionService.GetRevision(ctx, revisionID) + if err != nil { + return nil, err + } + objInfo, err := as.objectInfoService.GetInfo(ctx, revision.ObjectID) + if err != nil { + return nil, err + } + + switch objInfo.ObjectType { + case constant.QuestionObjectType: + data := &entity.QuestionWithTagsRevision{} + if err = json.Unmarshal([]byte(revision.Content), data); err != nil { + log.Errorf("revision parsing error %s", err) + return resp, nil + } + for _, tag := range data.Tags { + resp.Tags = append(resp.Tags, tag.SlugName) + } + resp.Title = data.Title + resp.OriginalText = data.OriginalText + case constant.AnswerObjectType: + data := &entity.Answer{} + if err = json.Unmarshal([]byte(revision.Content), data); err != nil { + log.Errorf("revision parsing error %s", err) + return resp, nil + } + resp.Title = objInfo.Title // answer show question title + resp.OriginalText = data.OriginalText + case constant.TagObjectType: + data := &entity.Tag{} + if err = json.Unmarshal([]byte(revision.Content), data); err != nil { + log.Errorf("revision parsing error %s", err) + return resp, nil + } + resp.Title = data.SlugName + resp.OriginalText = data.OriginalText + default: + log.Errorf("unknown object type %s", objInfo.ObjectType) + } + return resp, nil +} + +func formatActivity(activityType string) (isHidden bool, formattedActivityType string) { + if activityType == "voted_up" || activityType == "voted_down" || activityType == "accepted" { + return true, "" + } + if activityType == "vote_up" { + return false, "upvote" + } + if activityType == "vote_down" { + return false, "downvote" + } + return false, activityType +} diff --git a/internal/service/activity/answer_activity.go b/internal/service/activity/answer_activity.go index 30933f08..2a8e51d8 100644 --- a/internal/service/activity/answer_activity.go +++ b/internal/service/activity/answer_activity.go @@ -10,9 +10,9 @@ import ( // AnswerActivityRepo answer activity type AnswerActivityRepo interface { AcceptAnswer(ctx context.Context, - answerObjID, questionUserID, answerUserID string, isSelf bool) (err error) + answerObjID, questionObjID, questionUserID, answerUserID string, isSelf bool) (err error) CancelAcceptAnswer(ctx context.Context, - answerObjID, questionUserID, answerUserID string) (err error) + answerObjID, questionObjID, questionUserID, answerUserID string) (err error) DeleteAnswer(ctx context.Context, answerID string) (err error) } @@ -38,14 +38,14 @@ func NewAnswerActivityService( // AcceptAnswer accept answer change activity func (as *AnswerActivityService) AcceptAnswer(ctx context.Context, - answerObjID, questionUserID, answerUserID string, isSelf bool) (err error) { - return as.answerActivityRepo.AcceptAnswer(ctx, answerObjID, questionUserID, answerUserID, isSelf) + 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, questionUserID, answerUserID string) (err error) { - return as.answerActivityRepo.CancelAcceptAnswer(ctx, answerObjID, questionUserID, answerUserID) + answerObjID, questionObjID, questionUserID, answerUserID string) (err error) { + return as.answerActivityRepo.CancelAcceptAnswer(ctx, answerObjID, questionObjID, questionUserID, answerUserID) } // DeleteAnswer delete answer change activity diff --git a/internal/service/activity_common/activity.go b/internal/service/activity_common/activity.go index 0b35c478..8d76ac2c 100644 --- a/internal/service/activity_common/activity.go +++ b/internal/service/activity_common/activity.go @@ -4,6 +4,9 @@ import ( "context" "github.com/answerdev/answer/internal/entity" + "github.com/answerdev/answer/internal/service/activity_queue" + "github.com/answerdev/answer/pkg/converter" + "github.com/segmentfault/pacman/log" "xorm.io/xorm" ) @@ -13,4 +16,56 @@ type ActivityRepo interface { 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) + GetActivityTypeByConfigKey(ctx context.Context, configKey string) (activityType int, err error) + AddActivity(ctx context.Context, activity *entity.Activity) (err error) +} + +type ActivityCommon struct { + activityRepo ActivityRepo +} + +// NewActivityCommon new activity common +func NewActivityCommon( + activityRepo ActivityRepo, +) *ActivityCommon { + activity := &ActivityCommon{ + activityRepo: activityRepo, + } + activity.HandleActivity() + return activity +} + +// HandleActivity handle activity message +func (ac *ActivityCommon) HandleActivity() { + go func() { + defer func() { + if err := recover(); err != nil { + log.Error(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: msg.ObjectID, + OriginalObjectID: 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) + } + } + }() } diff --git a/internal/service/activity_queue/activity_queue.go b/internal/service/activity_queue/activity_queue.go new file mode 100644 index 00000000..27897268 --- /dev/null +++ b/internal/service/activity_queue/activity_queue.go @@ -0,0 +1,14 @@ +package activity_queue + +import ( + "github.com/answerdev/answer/internal/schema" +) + +var ( + ActivityQueue = make(chan *schema.ActivityMsg, 128) +) + +// AddActivity add new activity +func AddActivity(msg *schema.ActivityMsg) { + ActivityQueue <- msg +} diff --git a/internal/service/answer_common/answer.go b/internal/service/answer_common/answer.go index 196d6628..cce4e9eb 100644 --- a/internal/service/answer_common/answer.go +++ b/internal/service/answer_common/answer.go @@ -68,7 +68,11 @@ func (as *AnswerCommon) ShowFormat(ctx context.Context, data *entity.Answer) *sc info.VoteCount = data.VoteCount info.CreateTime = data.CreatedAt.Unix() info.UpdateTime = data.UpdatedAt.Unix() + if data.UpdatedAt.Unix() < 1 { + info.UpdateTime = 0 + } info.UserID = data.UserID + info.UpdateUserID = data.LastEditUserID return &info } @@ -80,7 +84,11 @@ func (as *AnswerCommon) AdminShowFormat(ctx context.Context, data *entity.Answer info.VoteCount = data.VoteCount info.CreateTime = data.CreatedAt.Unix() info.UpdateTime = data.UpdatedAt.Unix() + if data.UpdatedAt.Unix() < 1 { + info.UpdateTime = 0 + } info.UserID = data.UserID + info.UpdateUserID = data.LastEditUserID info.Description = htmltext.FetchExcerpt(data.ParsedText, "...", 240) return &info } diff --git a/internal/service/answer_service.go b/internal/service/answer_service.go index 30f8e42d..26558c4b 100644 --- a/internal/service/answer_service.go +++ b/internal/service/answer_service.go @@ -9,6 +9,7 @@ import ( "github.com/answerdev/answer/internal/base/constant" "github.com/answerdev/answer/internal/service/activity" "github.com/answerdev/answer/internal/service/activity_common" + "github.com/answerdev/answer/internal/service/activity_queue" "github.com/answerdev/answer/internal/service/notice_queue" "github.com/answerdev/answer/internal/service/revision_common" @@ -73,27 +74,29 @@ func (as *AnswerService) RemoveAnswer(ctx context.Context, req *schema.RemoveAns if !exist { return nil } - if answerInfo.UserID != req.UserID { - return errors.BadRequest(reason.UnauthorizedError) - } - if answerInfo.VoteCount > 0 { - return errors.BadRequest(reason.UnauthorizedError) - } - if answerInfo.Adopted == schema.AnswerAdoptedEnable { - return errors.BadRequest(reason.UnauthorizedError) - } - questionInfo, exist, err := as.questionRepo.GetQuestion(ctx, answerInfo.QuestionID) - if err != nil { - return errors.BadRequest(reason.UnauthorizedError) - } - if !exist { - return errors.BadRequest(reason.UnauthorizedError) - } - if questionInfo.AnswerCount > 1 { - return errors.BadRequest(reason.UnauthorizedError) - } - if questionInfo.AcceptedAnswerID != "" { - return errors.BadRequest(reason.UnauthorizedError) + if !req.IsAdmin { + if answerInfo.UserID != req.UserID { + return errors.BadRequest(reason.AnswerCannotDeleted) + } + if answerInfo.VoteCount > 0 { + return errors.BadRequest(reason.AnswerCannotDeleted) + } + if answerInfo.Adopted == schema.AnswerAdoptedEnable { + return errors.BadRequest(reason.AnswerCannotDeleted) + } + questionInfo, exist, err := as.questionRepo.GetQuestion(ctx, answerInfo.QuestionID) + if err != nil { + return errors.BadRequest(reason.AnswerCannotDeleted) + } + if !exist { + return errors.BadRequest(reason.AnswerCannotDeleted) + } + if questionInfo.AnswerCount > 1 { + return errors.BadRequest(reason.AnswerCannotDeleted) + } + if questionInfo.AcceptedAnswerID != "" { + return errors.BadRequest(reason.AnswerCannotDeleted) + } } // user add question count @@ -126,7 +129,6 @@ func (as *AnswerService) Insert(ctx context.Context, req *schema.AnswerAddReq) ( if !exist { return "", errors.BadRequest(reason.QuestionNotFound) } - now := time.Now() insertData := new(entity.Answer) insertData.UserID = req.UserID insertData.OriginalText = req.Content @@ -135,7 +137,7 @@ func (as *AnswerService) Insert(ctx context.Context, req *schema.AnswerAddReq) ( insertData.QuestionID = req.QuestionID insertData.RevisionID = "0" insertData.Status = entity.AnswerStatusAvailable - insertData.UpdatedAt = now + //insertData.UpdatedAt = now if err = as.answerRepo.AddAnswer(ctx, insertData); err != nil { return "", err } @@ -164,15 +166,40 @@ func (as *AnswerService) Insert(ctx context.Context, req *schema.AnswerAddReq) ( } infoJSON, _ := json.Marshal(insertData) revisionDTO.Content = string(infoJSON) - err = as.revisionService.AddRevision(ctx, revisionDTO, true) + revisionID, err := as.revisionService.AddRevision(ctx, revisionDTO, true) if err != nil { return insertData.ID, err } as.notificationAnswerTheQuestion(ctx, questionInfo.UserID, insertData.ID, req.UserID) + + activity_queue.AddActivity(&schema.ActivityMsg{ + UserID: insertData.UserID, + ObjectID: insertData.ID, + OriginalObjectID: insertData.ID, + ActivityTypeKey: constant.ActAnswerAnswered, + RevisionID: revisionID, + }) + activity_queue.AddActivity(&schema.ActivityMsg{ + UserID: insertData.UserID, + ObjectID: insertData.ID, + OriginalObjectID: questionInfo.ID, + ActivityTypeKey: constant.ActQuestionAnswered, + }) return insertData.ID, nil } func (as *AnswerService) Update(ctx context.Context, req *schema.AnswerUpdateReq) (string, error) { + //req.NoNeedReview //true 不需要审核 + var canUpdate bool + _, existUnreviewed, err := as.revisionService.ExistUnreviewedByObjectID(ctx, req.ID) + if err != nil { + return "", err + } + if existUnreviewed { + err = errors.BadRequest(reason.AnswerCannotUpdate) + return "", err + } + questionInfo, exist, err := as.questionRepo.GetQuestion(ctx, req.QuestionID) if err != nil { return "", err @@ -180,34 +207,75 @@ func (as *AnswerService) Update(ctx context.Context, req *schema.AnswerUpdateReq if !exist { return "", errors.BadRequest(reason.QuestionNotFound) } + + answerInfo, exist, err := as.answerRepo.GetByID(ctx, req.ID) + if err != nil { + return "", err + } + if !exist { + return "", nil + } + + //If the content is the same, ignore it + if answerInfo.OriginalText == req.Content { + return "", nil + } + now := time.Now() insertData := new(entity.Answer) insertData.ID = req.ID + insertData.UserID = answerInfo.UserID insertData.QuestionID = req.QuestionID - insertData.UserID = req.UserID insertData.OriginalText = req.Content insertData.ParsedText = req.HTML insertData.UpdatedAt = now - if err = as.answerRepo.UpdateAnswer(ctx, insertData, []string{"original_text", "parsed_text", "update_time"}); err != nil { - return "", err - } - err = as.questionCommon.UpdataPostTime(ctx, req.QuestionID) - if err != nil { - return insertData.ID, err + + insertData.LastEditUserID = "0" + if answerInfo.UserID != req.UserID { + insertData.LastEditUserID = req.UserID } + revisionDTO := &schema.AddRevisionDTO{ UserID: req.UserID, ObjectID: req.ID, Title: "", Log: req.EditSummary, } + + if req.NoNeedReview || answerInfo.UserID == req.UserID { + canUpdate = true + } + + if !canUpdate { + revisionDTO.Status = entity.RevisionUnreviewedStatus + } else { + if err = as.answerRepo.UpdateAnswer(ctx, insertData, []string{"original_text", "parsed_text", "update_time"}); err != nil { + return "", err + } + err = as.questionCommon.UpdataPostTime(ctx, req.QuestionID) + if err != nil { + return insertData.ID, err + } + as.notificationUpdateAnswer(ctx, questionInfo.UserID, insertData.ID, req.UserID) + revisionDTO.Status = entity.RevisionReviewPassStatus + } + infoJSON, _ := json.Marshal(insertData) revisionDTO.Content = string(infoJSON) - err = as.revisionService.AddRevision(ctx, revisionDTO, true) + revisionID, err := as.revisionService.AddRevision(ctx, revisionDTO, true) if err != nil { return insertData.ID, err } - as.notificationUpdateAnswer(ctx, questionInfo.UserID, insertData.ID, req.UserID) + if canUpdate { + activity_queue.AddActivity(&schema.ActivityMsg{ + UserID: insertData.UserID, + ObjectID: insertData.ID, + OriginalObjectID: insertData.ID, + ActivityTypeKey: constant.ActAnswerEdited, + RevisionID: revisionID, + }) + } + return insertData.ID, nil } @@ -276,14 +344,14 @@ func (as *AnswerService) updateAnswerRank(ctx context.Context, userID string, // if this question is already been answered, should cancel old answer rank if oldAnswerInfo != nil { err := as.answerActivityService.CancelAcceptAnswer( - ctx, questionInfo.AcceptedAnswerID, questionInfo.UserID, oldAnswerInfo.UserID) + ctx, questionInfo.AcceptedAnswerID, questionInfo.ID, questionInfo.UserID, oldAnswerInfo.UserID) if err != nil { log.Error(err) } } if newAnswerInfo.ID != "" { err := as.answerActivityService.AcceptAnswer( - ctx, newAnswerInfo.ID, questionInfo.UserID, newAnswerInfo.UserID, newAnswerInfo.UserID == userID) + ctx, newAnswerInfo.ID, questionInfo.ID, questionInfo.UserID, newAnswerInfo.UserID, newAnswerInfo.UserID == userID) if err != nil { log.Error(err) } @@ -348,7 +416,7 @@ func (as *AnswerService) AdminSetAnswerStatus(ctx context.Context, answerID stri } if setStatus == entity.AnswerStatusDeleted { - err = as.answerActivityService.DeleteQuestion(ctx, answerInfo.ID, answerInfo.CreatedAt, answerInfo.VoteCount) + err = as.answerActivityService.DeleteAnswer(ctx, answerInfo.ID, answerInfo.CreatedAt, answerInfo.VoteCount) if err != nil { log.Errorf("admin delete question then rank rollback error %s", err.Error()) } @@ -366,39 +434,40 @@ func (as *AnswerService) AdminSetAnswerStatus(ctx context.Context, answerID stri return nil } -func (as *AnswerService) SearchList(ctx context.Context, search *schema.AnswerList) ([]*schema.AnswerInfo, int64, error) { +func (as *AnswerService) SearchList(ctx context.Context, req *schema.AnswerListReq) ([]*schema.AnswerInfo, int64, error) { list := make([]*schema.AnswerInfo, 0) dbSearch := entity.AnswerSearch{} - dbSearch.QuestionID = search.QuestionID - dbSearch.Page = search.Page - dbSearch.PageSize = search.PageSize - dbSearch.Order = search.Order - dblist, count, err := as.answerRepo.SearchList(ctx, &dbSearch) + dbSearch.QuestionID = req.QuestionID + dbSearch.Page = req.Page + dbSearch.PageSize = req.PageSize + dbSearch.Order = req.Order + answerOriginalList, count, err := as.answerRepo.SearchList(ctx, &dbSearch) if err != nil { return list, count, err } - AnswerList, err := as.SearchFormatInfo(ctx, dblist, search.LoginUserID) + answerList, err := as.SearchFormatInfo(ctx, answerOriginalList, req) if err != nil { - return AnswerList, count, err + return answerList, count, err } - return AnswerList, count, nil + return answerList, count, nil } -func (as *AnswerService) SearchFormatInfo(ctx context.Context, dblist []*entity.Answer, loginUserID string) ([]*schema.AnswerInfo, error) { +func (as *AnswerService) SearchFormatInfo(ctx context.Context, answers []*entity.Answer, req *schema.AnswerListReq) ( + []*schema.AnswerInfo, error) { list := make([]*schema.AnswerInfo, 0) - objectIds := make([]string, 0) - userIds := make([]string, 0) - for _, dbitem := range dblist { - item := as.ShowFormat(ctx, dbitem) + objectIDs := make([]string, 0) + userIDs := make([]string, 0) + for _, info := range answers { + item := as.ShowFormat(ctx, info) list = append(list, item) - objectIds = append(objectIds, dbitem.ID) - userIds = append(userIds, dbitem.UserID) - if loginUserID != "" { - // item.VoteStatus = as.activityFunc.GetVoteStatus(ctx, item.TagID, loginUserId) - item.VoteStatus = as.voteRepo.GetVoteStatus(ctx, item.ID, loginUserID) + objectIDs = append(objectIDs, info.ID) + userIDs = append(userIDs, info.UserID) + userIDs = append(userIDs, info.LastEditUserID) + if req.UserID != "" { + item.VoteStatus = as.voteRepo.GetVoteStatus(ctx, item.ID, req.UserID) } } - userInfoMap, err := as.userCommon.BatchUserBasicInfoByID(ctx, userIds) + userInfoMap, err := as.userCommon.BatchUserBasicInfoByID(ctx, userIDs) if err != nil { return list, err } @@ -406,30 +475,32 @@ func (as *AnswerService) SearchFormatInfo(ctx context.Context, dblist []*entity. _, ok := userInfoMap[item.UserID] if ok { item.UserInfo = userInfoMap[item.UserID] - item.UpdateUserInfo = userInfoMap[item.UserID] + } + _, ok = userInfoMap[item.UpdateUserID] + if ok { + item.UpdateUserInfo = userInfoMap[item.UpdateUserID] } } - if loginUserID == "" { + if req.UserID == "" { return list, nil } - CollectedMap, err := as.collectionCommon.SearchObjectCollected(ctx, loginUserID, objectIds) + searchObjectCollected, err := as.collectionCommon.SearchObjectCollected(ctx, req.UserID, objectIDs) if err != nil { - log.Error("CollectionFunc.SearchObjectCollected error", err) + return nil, err } for _, item := range list { - _, ok := CollectedMap[item.ID] + _, ok := searchObjectCollected[item.ID] if ok { item.Collected = true } } for _, item := range list { - item.MemberActions = permission.GetAnswerPermission(loginUserID, item.UserID) + item.MemberActions = permission.GetAnswerPermission(ctx, req.UserID, item.UserID, req.CanEdit, req.CanDelete) } - return list, nil } diff --git a/internal/service/auth/auth.go b/internal/service/auth/auth.go index 23f4c010..1f77d174 100644 --- a/internal/service/auth/auth.go +++ b/internal/service/auth/auth.go @@ -40,7 +40,7 @@ func (as *AuthService) GetUserCacheInfo(ctx context.Context, accessToken string) } cacheInfo, _ := as.authRepo.GetUserStatus(ctx, userCacheInfo.UserID) if cacheInfo != nil { - log.Infof("user status updated: %+v", cacheInfo) + log.Debugf("user status updated: %+v", cacheInfo) userCacheInfo.UserStatus = cacheInfo.UserStatus userCacheInfo.EmailStatus = cacheInfo.EmailStatus userCacheInfo.IsAdmin = cacheInfo.IsAdmin diff --git a/internal/service/comment/comment_service.go b/internal/service/comment/comment_service.go index a7156c76..1af7afa3 100644 --- a/internal/service/comment/comment_service.go +++ b/internal/service/comment/comment_service.go @@ -9,9 +9,10 @@ import ( "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/activity_queue" "github.com/answerdev/answer/internal/service/comment_common" "github.com/answerdev/answer/internal/service/notice_queue" - object_info "github.com/answerdev/answer/internal/service/object_info" + "github.com/answerdev/answer/internal/service/object_info" "github.com/answerdev/answer/internal/service/permission" usercommon "github.com/answerdev/answer/internal/service/user_common" "github.com/jinzhu/copier" @@ -111,9 +112,9 @@ func (cs *CommentService) AddComment(ctx context.Context, req *schema.AddComment } if objInfo.ObjectType == constant.QuestionObjectType { - cs.notificationQuestionComment(ctx, objInfo.ObjectCreator, comment.ID, req.UserID) + cs.notificationQuestionComment(ctx, objInfo.ObjectCreatorUserID, comment.ID, req.UserID) } else if objInfo.ObjectType == constant.AnswerObjectType { - cs.notificationAnswerComment(ctx, objInfo.ObjectCreator, comment.ID, req.UserID) + cs.notificationAnswerComment(ctx, objInfo.ObjectCreatorUserID, comment.ID, req.UserID) } if len(req.MentionUsernameList) > 0 { cs.notificationMention(ctx, req.MentionUsernameList, comment.ID, req.UserID) @@ -121,7 +122,7 @@ func (cs *CommentService) AddComment(ctx context.Context, req *schema.AddComment resp = &schema.GetCommentResp{} resp.SetFromComment(comment) - resp.MemberActions = permission.GetCommentPermission(req.UserID, resp.UserID) + resp.MemberActions = permission.GetCommentPermission(ctx, req.UserID, resp.UserID, req.CanEdit, req.CanDelete) // get reply user info if len(resp.ReplyUserID) > 0 { @@ -148,6 +149,13 @@ func (cs *CommentService) AddComment(ctx context.Context, req *schema.AddComment resp.UserAvatar = userInfo.Avatar resp.UserStatus = userInfo.Status } + + activity_queue.AddActivity(&schema.ActivityMsg{ + UserID: comment.UserID, + ObjectID: comment.ID, + OriginalObjectID: req.ObjectID, + ActivityTypeKey: constant.ActQuestionCommented, + }) return resp, nil } @@ -222,7 +230,7 @@ func (cs *CommentService) GetComment(ctx context.Context, req *schema.GetComment // check if current user vote this comment resp.IsVote = cs.checkIsVote(ctx, req.UserID, resp.CommentID) - resp.MemberActions = permission.GetCommentPermission(req.UserID, resp.UserID) + resp.MemberActions = permission.GetCommentPermission(ctx, req.UserID, resp.UserID, req.CanEdit, req.CanDelete) return resp, nil } @@ -282,7 +290,7 @@ func (cs *CommentService) GetCommentWithPage(ctx context.Context, req *schema.Ge // check if current user vote this comment commentResp.IsVote = cs.checkIsVote(ctx, req.UserID, commentResp.CommentID) - commentResp.MemberActions = permission.GetCommentPermission(req.UserID, commentResp.UserID) + commentResp.MemberActions = permission.GetCommentPermission(ctx, req.UserID, commentResp.UserID, req.CanEdit, req.CanDelete) resp = append(resp, commentResp) } return pager.NewPageModel(total, resp), nil diff --git a/internal/service/notification/notification_service.go b/internal/service/notification/notification_service.go index f0a9cc1f..17534f8c 100644 --- a/internal/service/notification/notification_service.go +++ b/internal/service/notification/notification_service.go @@ -11,6 +11,8 @@ import ( "github.com/answerdev/answer/internal/base/translator" "github.com/answerdev/answer/internal/schema" notficationcommon "github.com/answerdev/answer/internal/service/notification_common" + "github.com/answerdev/answer/internal/service/revision_common" + "github.com/jinzhu/copier" "github.com/segmentfault/pacman/i18n" "github.com/segmentfault/pacman/log" ) @@ -20,24 +22,28 @@ type NotificationService struct { data *data.Data notificationRepo notficationcommon.NotificationRepo notificationCommon *notficationcommon.NotificationCommon + revisionService *revision_common.RevisionService } func NewNotificationService( data *data.Data, notificationRepo notficationcommon.NotificationRepo, notificationCommon *notficationcommon.NotificationCommon, + revisionService *revision_common.RevisionService, + ) *NotificationService { return &NotificationService{ data: data, notificationRepo: notificationRepo, notificationCommon: notificationCommon, + revisionService: revisionService, } } -func (ns *NotificationService) GetRedDot(ctx context.Context, userID string) (*schema.RedDot, error) { +func (ns *NotificationService) GetRedDot(ctx context.Context, req *schema.GetRedDot) (*schema.RedDot, error) { redBot := &schema.RedDot{} - inboxKey := fmt.Sprintf("answer_RedDot_%d_%s", schema.NotificationTypeInbox, userID) - achievementKey := fmt.Sprintf("answer_RedDot_%d_%s", schema.NotificationTypeAchievement, userID) + inboxKey := fmt.Sprintf("answer_RedDot_%d_%s", schema.NotificationTypeInbox, req.UserID) + achievementKey := fmt.Sprintf("answer_RedDot_%d_%s", schema.NotificationTypeAchievement, req.UserID) inboxValue, err := ns.data.Cache.GetInt64(ctx, inboxKey) if err != nil { redBot.Inbox = 0 @@ -50,19 +56,32 @@ func (ns *NotificationService) GetRedDot(ctx context.Context, userID string) (*s } else { redBot.Achievement = achievementValue } + revisionCount := &schema.RevisionSearch{} + _ = copier.Copy(revisionCount, req) + if req.CanReviewAnswer || req.CanReviewQuestion || req.CanReviewTag { + redBot.CanRevision = true + revisionCountNum, err := ns.revisionService.GetUnreviewedRevisionCount(ctx, revisionCount) + if err != nil { + return redBot, err + } + redBot.Revision = revisionCountNum + } + return redBot, nil } -func (ns *NotificationService) ClearRedDot(ctx context.Context, userID string, botTypeStr string) (*schema.RedDot, error) { - botType, ok := schema.NotificationType[botTypeStr] +func (ns *NotificationService) ClearRedDot(ctx context.Context, req *schema.NotificationClearRequest) (*schema.RedDot, error) { + botType, ok := schema.NotificationType[req.TypeStr] if ok { - key := fmt.Sprintf("answer_RedDot_%d_%s", botType, userID) + key := fmt.Sprintf("answer_RedDot_%d_%s", botType, req.UserID) err := ns.data.Cache.Del(ctx, key) if err != nil { log.Error("ClearRedDot del cache error", err.Error()) } } - return ns.GetRedDot(ctx, userID) + getRedDotreq := &schema.GetRedDot{} + _ = copier.Copy(getRedDotreq, req) + return ns.GetRedDot(ctx, getRedDotreq) } func (ns *NotificationService) ClearUnRead(ctx context.Context, userID string, botTypeStr string) error { diff --git a/internal/service/object_info/object_info.go b/internal/service/object_info/object_info.go index aea2208d..52bc4658 100644 --- a/internal/service/object_info/object_info.go +++ b/internal/service/object_info/object_info.go @@ -20,6 +20,7 @@ type ObjService struct { questionRepo questioncommon.QuestionRepo commentRepo comment_common.CommentCommonRepo tagRepo tagcommon.TagCommonRepo + tagCommon *tagcommon.TagCommonService } // NewObjService new object service @@ -27,14 +28,92 @@ func NewObjService( answerRepo answercommon.AnswerRepo, questionRepo questioncommon.QuestionRepo, commentRepo comment_common.CommentCommonRepo, - tagRepo tagcommon.TagCommonRepo) *ObjService { + tagRepo tagcommon.TagCommonRepo, + tagCommon *tagcommon.TagCommonService, +) *ObjService { return &ObjService{ answerRepo: answerRepo, questionRepo: questionRepo, commentRepo: commentRepo, tagRepo: tagRepo, + tagCommon: tagCommon, } } +func (os *ObjService) GetUnreviewedRevisionInfo(ctx context.Context, objectID string) (objInfo *schema.UnreviewedRevisionInfoInfo, err error) { + objInfo = &schema.UnreviewedRevisionInfoInfo{} + + objectType, err := obj.GetObjectTypeStrByObjectID(objectID) + if err != nil { + return nil, err + } + switch objectType { + case constant.QuestionObjectType: + questionInfo, exist, err := os.questionRepo.GetQuestion(ctx, objectID) + if err != nil { + return nil, err + } + if !exist { + break + } + taglist, err := os.tagCommon.GetObjectEntityTag(ctx, objectID) + if err != nil { + return nil, err + } + os.tagCommon.TagsFormatRecommendAndReserved(ctx, taglist) + tags, err := os.tagCommon.TagFormat(ctx, taglist) + if err != nil { + return nil, err + } + objInfo = &schema.UnreviewedRevisionInfoInfo{ + ObjectID: questionInfo.ID, + Title: questionInfo.Title, + Content: questionInfo.OriginalText, + Html: questionInfo.ParsedText, + Tags: tags, + } + case constant.AnswerObjectType: + answerInfo, exist, err := os.answerRepo.GetAnswer(ctx, objectID) + if err != nil { + return nil, err + } + if !exist { + break + } + + questionInfo, exist, err := os.questionRepo.GetQuestion(ctx, answerInfo.QuestionID) + if err != nil { + return nil, err + } + if !exist { + break + } + objInfo = &schema.UnreviewedRevisionInfoInfo{ + ObjectID: answerInfo.ID, + Title: questionInfo.Title, + Content: answerInfo.OriginalText, + Html: answerInfo.ParsedText, + } + + case constant.TagObjectType: + tagInfo, exist, err := os.tagRepo.GetTagByID(ctx, objectID) + if err != nil { + return nil, err + } + if !exist { + break + } + objInfo = &schema.UnreviewedRevisionInfoInfo{ + ObjectID: tagInfo.ID, + Title: tagInfo.SlugName, + Content: tagInfo.OriginalText, + Html: tagInfo.ParsedText, + } + } + if objInfo == nil { + err = errors.BadRequest(reason.ObjectNotFound) + } + return objInfo, err +} // GetInfo get object simple information func (os *ObjService) GetInfo(ctx context.Context, objectID string) (objInfo *schema.SimpleObjectInfo, err error) { @@ -52,12 +131,12 @@ func (os *ObjService) GetInfo(ctx context.Context, objectID string) (objInfo *sc break } objInfo = &schema.SimpleObjectInfo{ - ObjectID: questionInfo.ID, - ObjectCreator: questionInfo.UserID, - QuestionID: questionInfo.ID, - ObjectType: objectType, - Title: questionInfo.Title, - Content: questionInfo.ParsedText, // todo trim + ObjectID: questionInfo.ID, + ObjectCreatorUserID: questionInfo.UserID, + QuestionID: questionInfo.ID, + ObjectType: objectType, + Title: questionInfo.Title, + Content: questionInfo.ParsedText, // todo trim } case constant.AnswerObjectType: answerInfo, exist, err := os.answerRepo.GetAnswer(ctx, objectID) @@ -72,13 +151,13 @@ func (os *ObjService) GetInfo(ctx context.Context, objectID string) (objInfo *sc return nil, err } objInfo = &schema.SimpleObjectInfo{ - ObjectID: answerInfo.ID, - ObjectCreator: answerInfo.UserID, - QuestionID: answerInfo.QuestionID, - AnswerID: answerInfo.ID, - ObjectType: objectType, - Title: questionInfo.Title, // this should be question title - Content: answerInfo.ParsedText, // todo trim + ObjectID: answerInfo.ID, + ObjectCreatorUserID: answerInfo.UserID, + QuestionID: answerInfo.QuestionID, + AnswerID: answerInfo.ID, + ObjectType: objectType, + Title: questionInfo.Title, // this should be question title + Content: answerInfo.ParsedText, // todo trim } case constant.CommentObjectType: commentInfo, exist, err := os.commentRepo.GetComment(ctx, objectID) @@ -89,11 +168,11 @@ func (os *ObjService) GetInfo(ctx context.Context, objectID string) (objInfo *sc break } objInfo = &schema.SimpleObjectInfo{ - ObjectID: commentInfo.ID, - ObjectCreator: commentInfo.UserID, - ObjectType: objectType, - Content: commentInfo.ParsedText, // todo trim - CommentID: commentInfo.ID, + ObjectID: commentInfo.ID, + ObjectCreatorUserID: commentInfo.UserID, + ObjectType: objectType, + Content: commentInfo.ParsedText, // todo trim + CommentID: commentInfo.ID, } if len(commentInfo.QuestionID) > 0 { questionInfo, exist, err := os.questionRepo.GetQuestion(ctx, commentInfo.QuestionID) diff --git a/internal/service/permission/comment_permission.go b/internal/service/permission/comment_permission.go index ba04e0c3..663bb5b3 100644 --- a/internal/service/permission/comment_permission.go +++ b/internal/service/permission/comment_permission.go @@ -1,9 +1,13 @@ package permission -import "github.com/answerdev/answer/internal/schema" +import ( + "context" -// TODO: There is currently no permission management -func GetCommentPermission(userID string, commentCreatorUserID string) ( + "github.com/answerdev/answer/internal/schema" +) + +// GetCommentPermission get comment permission +func GetCommentPermission(ctx context.Context, userID string, creatorUserID string, canEdit, canDelete bool) ( actions []*schema.PermissionMemberAction) { actions = make([]*schema.PermissionMemberAction, 0) if len(userID) > 0 { @@ -13,44 +17,48 @@ func GetCommentPermission(userID string, commentCreatorUserID string) ( Type: "reason", }) } - if userID != commentCreatorUserID { - return actions - } - actions = append(actions, []*schema.PermissionMemberAction{ - { + if canEdit || userID == creatorUserID { + actions = append(actions, &schema.PermissionMemberAction{ Action: "edit", Name: "Edit", Type: "edit", - }, - { + }) + } + + if canDelete || userID == creatorUserID { + actions = append(actions, &schema.PermissionMemberAction{ Action: "delete", Name: "Delete", Type: "reason", - }, - }...) + }) + } return actions } -func GetTagPermission(userID string, tagCreatorUserID string) ( +// GetTagPermission get tag permission +func GetTagPermission(ctx context.Context, canEdit, canDelete bool) ( actions []*schema.PermissionMemberAction) { - if userID != tagCreatorUserID { - return []*schema.PermissionMemberAction{} - } - return []*schema.PermissionMemberAction{ - { + actions = make([]*schema.PermissionMemberAction, 0) + if canEdit { + actions = append(actions, &schema.PermissionMemberAction{ Action: "edit", Name: "Edit", Type: "edit", - }, - { + }) + } + + if canDelete { + actions = append(actions, &schema.PermissionMemberAction{ Action: "delete", Name: "Delete", Type: "reason", - }, + }) } + return actions } -func GetAnswerPermission(userID string, answerAuthID string) ( +// GetAnswerPermission get answer permission +func GetAnswerPermission(ctx context.Context, userID string, creatorUserID string, canEdit, canDelete bool) ( actions []*schema.PermissionMemberAction) { actions = make([]*schema.PermissionMemberAction, 0) if len(userID) > 0 { @@ -60,25 +68,26 @@ func GetAnswerPermission(userID string, answerAuthID string) ( Type: "reason", }) } - if userID != answerAuthID { - return actions - } - actions = append(actions, []*schema.PermissionMemberAction{ - { + if canEdit || userID == creatorUserID { + actions = append(actions, &schema.PermissionMemberAction{ Action: "edit", Name: "Edit", Type: "edit", - }, - { + }) + } + + if canDelete || userID == creatorUserID { + actions = append(actions, &schema.PermissionMemberAction{ Action: "delete", Name: "Delete", Type: "confirm", - }, - }...) + }) + } return actions } -func GetQuestionPermission(userID string, questionAuthID string) ( +// GetQuestionPermission get question permission +func GetQuestionPermission(ctx context.Context, userID string, creatorUserID string, canEdit, canDelete, canClose bool) ( actions []*schema.PermissionMemberAction) { actions = make([]*schema.PermissionMemberAction, 0) if len(userID) > 0 { @@ -88,25 +97,40 @@ func GetQuestionPermission(userID string, questionAuthID string) ( Type: "reason", }) } - if userID != questionAuthID { - return actions - } - actions = append(actions, []*schema.PermissionMemberAction{ - { + if canEdit || userID == creatorUserID { + actions = append(actions, &schema.PermissionMemberAction{ Action: "edit", Name: "Edit", Type: "edit", - }, - { + }) + } + if canClose { + actions = append(actions, &schema.PermissionMemberAction{ Action: "close", Name: "Close", Type: "confirm", - }, - { + }) + } + if canDelete || userID == creatorUserID { + actions = append(actions, &schema.PermissionMemberAction{ Action: "delete", Name: "Delete", Type: "confirm", - }, - }...) + }) + } + return actions +} + +// GetTagSynonymPermission get tag synonym permission +func GetTagSynonymPermission(ctx context.Context, canEdit bool) ( + actions []*schema.PermissionMemberAction) { + actions = make([]*schema.PermissionMemberAction, 0) + if canEdit { + actions = append(actions, &schema.PermissionMemberAction{ + Action: "edit", + Name: "Edit", + Type: "edit", + }) + } return actions } diff --git a/internal/service/provider.go b/internal/service/provider.go index a4782304..7af91e59 100644 --- a/internal/service/provider.go +++ b/internal/service/provider.go @@ -3,6 +3,7 @@ package service import ( "github.com/answerdev/answer/internal/service/action" "github.com/answerdev/answer/internal/service/activity" + "github.com/answerdev/answer/internal/service/activity_common" 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" @@ -22,6 +23,7 @@ import ( "github.com/answerdev/answer/internal/service/report_backyard" "github.com/answerdev/answer/internal/service/report_handle_backyard" "github.com/answerdev/answer/internal/service/revision_common" + "github.com/answerdev/answer/internal/service/search_parser" "github.com/answerdev/answer/internal/service/role" "github.com/answerdev/answer/internal/service/search_parser" "github.com/answerdev/answer/internal/service/siteinfo" @@ -73,6 +75,8 @@ var ProviderSetService = wire.NewSet( notification.NewNotificationService, activity.NewAnswerActivityService, dashboard.NewDashboardService, + activity_common.NewActivityCommon, + activity.NewActivityService, role.NewRoleService, role.NewUserRoleRelService, role.NewRolePowerRelService, diff --git a/internal/service/question_common/question.go b/internal/service/question_common/question.go index e8b97e18..645d1148 100644 --- a/internal/service/question_common/question.go +++ b/internal/service/question_common/question.go @@ -5,8 +5,10 @@ import ( "encoding/json" "time" + "github.com/answerdev/answer/internal/base/constant" "github.com/answerdev/answer/internal/base/reason" "github.com/answerdev/answer/internal/service/activity_common" + "github.com/answerdev/answer/internal/service/activity_queue" "github.com/answerdev/answer/internal/service/config" "github.com/answerdev/answer/internal/service/meta" "github.com/segmentfault/pacman/errors" @@ -232,6 +234,9 @@ func (qs *QuestionCommon) ListFormat(ctx context.Context, questionList []*entity list = append(list, item) objectIds = append(objectIds, item.ID) userIds = append(userIds, questionInfo.UserID) + userIds = append(userIds, questionInfo.LastEditUserID) + userIds = append(userIds, item.LastAnsweredUserID) + } tagsMap, err := qs.tagCommon.BatchGetObjectTag(ctx, objectIds) if err != nil { @@ -252,7 +257,14 @@ func (qs *QuestionCommon) ListFormat(ctx context.Context, questionList []*entity if ok { item.UserInfo = userInfoMap[item.UserID] item.UpdateUserInfo = userInfoMap[item.UserID] - item.LastAnsweredUserInfo = userInfoMap[item.UserID] + } + _, ok = userInfoMap[item.LastEditUserID] + if ok { + item.UpdateUserInfo = userInfoMap[item.UserID] + } + _, ok = userInfoMap[item.LastAnsweredUserID] + if ok { + item.LastAnsweredUserInfo = userInfoMap[item.LastAnsweredUserID] } } @@ -308,7 +320,7 @@ func (qs *QuestionCommon) CloseQuestion(ctx context.Context, req *schema.CloseQu if !has { return nil } - questionInfo.Status = entity.QuestionStatusclosed + questionInfo.Status = entity.QuestionStatusClosed err = qs.questionRepo.UpdateQuestionStatus(ctx, questionInfo) if err != nil { return err @@ -322,6 +334,13 @@ func (qs *QuestionCommon) CloseQuestion(ctx context.Context, req *schema.CloseQu if err != nil { return err } + + activity_queue.AddActivity(&schema.ActivityMsg{ + UserID: questionInfo.UserID, + ObjectID: questionInfo.ID, + OriginalObjectID: questionInfo.ID, + ActivityTypeKey: constant.ActQuestionClosed, + }) return nil } @@ -371,9 +390,41 @@ func (qs *QuestionCommon) ShowFormat(ctx context.Context, data *entity.Question) info.CreateTime = data.CreatedAt.Unix() info.UpdateTime = data.UpdatedAt.Unix() info.PostUpdateTime = data.PostUpdateTime.Unix() + if data.PostUpdateTime.Unix() < 1 { + info.PostUpdateTime = 0 + } info.QuestionUpdateTime = data.UpdatedAt.Unix() + if data.UpdatedAt.Unix() < 1 { + info.QuestionUpdateTime = 0 + } info.Status = data.Status info.UserID = data.UserID + info.LastEditUserID = data.LastEditUserID + if data.LastAnswerID != "0" { + answerInfo, exist, err := qs.answerRepo.GetAnswer(ctx, data.LastAnswerID) + if err == nil && exist { + if answerInfo.LastEditUserID != "0" { + info.LastAnsweredUserID = answerInfo.LastEditUserID + } else { + info.LastAnsweredUserID = answerInfo.UserID + } + } + + } info.Tags = make([]*schema.TagResp, 0) return &info } +func (qs *QuestionCommon) ShowFormatWithTag(ctx context.Context, data *entity.QuestionWithTagsRevision) *schema.QuestionInfo { + info := qs.ShowFormat(ctx, &data.Question) + Tags := make([]*schema.TagResp, 0) + for _, tag := range data.Tags { + item := &schema.TagResp{} + item.SlugName = tag.SlugName + item.DisplayName = tag.DisplayName + item.Recommend = tag.Recommend + item.Reserved = tag.Reserved + Tags = append(Tags, item) + } + info.Tags = Tags + return info +} diff --git a/internal/service/question_service.go b/internal/service/question_service.go index 6f36fbe7..6997109e 100644 --- a/internal/service/question_service.go +++ b/internal/service/question_service.go @@ -7,11 +7,14 @@ import ( "time" "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/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/activity" + "github.com/answerdev/answer/internal/service/activity_queue" collectioncommon "github.com/answerdev/answer/internal/service/collection_common" "github.com/answerdev/answer/internal/service/meta" "github.com/answerdev/answer/internal/service/notice_queue" @@ -71,7 +74,13 @@ func (qs *QuestionService) CloseQuestion(ctx context.Context, req *schema.CloseQ if !has { return nil } - questionInfo.Status = entity.QuestionStatusclosed + + if !req.IsAdmin { + if questionInfo.UserID != req.UserID { + return errors.BadRequest(reason.QuestionCannotClose) + } + } + questionInfo.Status = entity.QuestionStatusClosed err = qs.questionRepo.UpdateQuestionStatus(ctx, questionInfo) if err != nil { return err @@ -85,6 +94,13 @@ func (qs *QuestionService) CloseQuestion(ctx context.Context, req *schema.CloseQ if err != nil { return err } + + activity_queue.AddActivity(&schema.ActivityMsg{ + UserID: req.UserID, + ObjectID: questionInfo.ID, + OriginalObjectID: questionInfo.ID, + ActivityTypeKey: constant.ActQuestionClosed, + }) return nil } @@ -105,18 +121,21 @@ func (qs *QuestionService) CloseMsgList(ctx context.Context, lang i18n.Language) } // AddQuestion add question -func (qs *QuestionService) AddQuestion(ctx context.Context, req *schema.QuestionAdd) (questionInfo *schema.QuestionInfo, err error) { +func (qs *QuestionService) AddQuestion(ctx context.Context, req *schema.QuestionAdd) (questionInfo any, err error) { recommendExist, err := qs.tagCommon.ExistRecommend(ctx, req.Tags) if err != nil { return } if !recommendExist { - err = fmt.Errorf("recommend is not exist") - err = errors.BadRequest(reason.RecommendTagNotExist).WithError(err).WithStack() - return + errorlist := make([]*validator.FormErrorField, 0) + errorlist = append(errorlist, &validator.FormErrorField{ + ErrorField: "tags", + ErrorMsg: translator.GlobalTrans.Tr(handler.GetLangByCtx(ctx), reason.RecommendTagEnter), + }) + err = errors.BadRequest(reason.RecommendTagEnter) + return errorlist, err } - questionInfo = &schema.QuestionInfo{} question := &entity.Question{} now := time.Now() question.UserID = req.UserID @@ -125,11 +144,11 @@ func (qs *QuestionService) AddQuestion(ctx context.Context, req *schema.Question question.ParsedText = req.HTML question.AcceptedAnswerID = "0" question.LastAnswerID = "0" - question.PostUpdateTime = now + //question.PostUpdateTime = nil question.Status = entity.QuestionStatusAvailable question.RevisionID = "0" question.CreatedAt = now - question.UpdatedAt = now + //question.UpdatedAt = nil err = qs.questionRepo.AddQuestion(ctx, question) if err != nil { return @@ -146,11 +165,25 @@ func (qs *QuestionService) AddQuestion(ctx context.Context, req *schema.Question revisionDTO := &schema.AddRevisionDTO{ UserID: question.UserID, ObjectID: question.ID, - Title: "", + Title: question.Title, } - infoJSON, _ := json.Marshal(question) + + tagNameList := make([]string, 0) + for _, tag := range req.Tags { + tagNameList = append(tagNameList, tag.SlugName) + } + Tags, tagerr := qs.tagCommon.GetTagListByNames(ctx, tagNameList) + if tagerr != nil { + return questionInfo, tagerr + } + + questionWithTagsRevision, err := qs.changeQuestionToRevision(ctx, question, Tags) + if err != nil { + return nil, err + } + infoJSON, _ := json.Marshal(questionWithTagsRevision) revisionDTO.Content = string(infoJSON) - err = qs.revisionService.AddRevision(ctx, revisionDTO, true) + revisionID, err := qs.revisionService.AddRevision(ctx, revisionDTO, true) if err != nil { return } @@ -161,7 +194,15 @@ func (qs *QuestionService) AddQuestion(ctx context.Context, req *schema.Question log.Error("user IncreaseQuestionCount error", err.Error()) } - questionInfo, err = qs.GetQuestion(ctx, question.ID, question.UserID, false) + activity_queue.AddActivity(&schema.ActivityMsg{ + UserID: question.UserID, + ObjectID: question.ID, + OriginalObjectID: question.ID, + ActivityTypeKey: constant.ActQuestionAsked, + RevisionID: revisionID, + }) + + questionInfo, err = qs.GetQuestion(ctx, question.ID, question.UserID, req.QuestionPermission) return } @@ -174,15 +215,31 @@ func (qs *QuestionService) RemoveQuestion(ctx context.Context, req *schema.Remov if !has { return nil } - if questionInfo.UserID != req.UserID { - return errors.BadRequest(reason.UnauthorizedError) - } + if !req.IsAdmin { + if questionInfo.UserID != req.UserID { + return errors.BadRequest(reason.QuestionCannotDeleted) + } - if questionInfo.AcceptedAnswerID != "" { - return errors.BadRequest(reason.UnauthorizedError) - } - if questionInfo.AnswerCount > 0 { - return errors.BadRequest(reason.UnauthorizedError) + if questionInfo.AcceptedAnswerID != "0" { + return errors.BadRequest(reason.QuestionCannotDeleted) + } + if questionInfo.AnswerCount > 1 { + return errors.BadRequest(reason.QuestionCannotDeleted) + } + + if questionInfo.AnswerCount == 1 { + answersearch := &entity.AnswerSearch{} + answersearch.QuestionID = req.ID + answerList, _, err := qs.questioncommon.AnswerCommon.Search(ctx, answersearch) + if err != nil { + return err + } + for _, answer := range answerList { + if answer.VoteCount > 0 { + return errors.BadRequest(reason.QuestionCannotDeleted) + } + } + } } questionInfo.Status = entity.QuestionStatusDeleted @@ -201,106 +258,198 @@ 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{ + UserID: questionInfo.UserID, + ObjectID: questionInfo.ID, + OriginalObjectID: questionInfo.ID, + ActivityTypeKey: constant.ActQuestionDeleted, + }) return nil } // UpdateQuestion update question -func (qs *QuestionService) UpdateQuestion(ctx context.Context, req *schema.QuestionUpdate) (questionInfo *schema.QuestionInfo, err error) { +func (qs *QuestionService) UpdateQuestion(ctx context.Context, req *schema.QuestionUpdate) (questionInfo any, err error) { + var canUpdate bool questionInfo = &schema.QuestionInfo{} - now := time.Now() - question := &entity.Question{} - question.UserID = req.UserID - question.Title = req.Title - question.OriginalText = req.Content - question.ParsedText = req.HTML - question.ID = req.ID - question.UpdatedAt = now - dbinfo, has, err := qs.questionRepo.GetQuestion(ctx, question.ID) + + _, existUnreviewed, err := qs.revisionService.ExistUnreviewedByObjectID(ctx, req.ID) + if err != nil { + return + + } + if existUnreviewed { + err = errors.BadRequest(reason.QuestionCannotUpdate) + return + } + + dbinfo, has, err := qs.questionRepo.GetQuestion(ctx, req.ID) if err != nil { return } if !has { return } + + now := time.Now() + question := &entity.Question{} + question.Title = req.Title + question.OriginalText = req.Content + question.ParsedText = req.HTML + question.ID = req.ID + question.UpdatedAt = now + question.PostUpdateTime = now + question.UserID = dbinfo.UserID + + question.LastEditUserID = "0" if dbinfo.UserID != req.UserID { - return + question.LastEditUserID = req.UserID } - //CheckChangeTag - oldTags, err := qs.tagCommon.GetObjectEntityTag(ctx, question.ID) - if err != nil { - return + oldTags, tagerr := qs.tagCommon.GetObjectEntityTag(ctx, question.ID) + if tagerr != nil { + return questionInfo, tagerr } + tagNameList := make([]string, 0) + oldtagNameList := make([]string, 0) for _, tag := range req.Tags { tagNameList = append(tagNameList, tag.SlugName) } - Tags, err := qs.tagCommon.GetTagListByNames(ctx, tagNameList) - if err != nil { - return + for _, tag := range oldTags { + oldtagNameList = append(oldtagNameList, tag.SlugName) } - CheckTag, CheckTaglist := qs.CheckChangeTag(ctx, oldTags, Tags) - if !CheckTag { - err = errors.BadRequest(reason.UnauthorizedError).WithMsg(fmt.Sprintf("tag [%s] cannot be modified", - strings.Join(CheckTaglist, ","))) + + isChange := qs.tagCommon.CheckTagsIsChange(ctx, tagNameList, oldtagNameList) + + //If the content is the same, ignore it + if dbinfo.Title == req.Title && dbinfo.OriginalText == req.Content && !isChange { return } - //update question to db - err = qs.questionRepo.UpdateQuestion(ctx, question, []string{"title", "original_text", "parsed_text", "updated_at"}) + Tags, tagerr := qs.tagCommon.GetTagListByNames(ctx, tagNameList) + if tagerr != nil { + return questionInfo, tagerr + } + + // If it's not admin + if !req.IsAdmin { + //CheckChangeTag + + CheckTag, CheckTaglist := qs.CheckChangeReservedTag(ctx, oldTags, Tags) + if !CheckTag { + errMsg := fmt.Sprintf(`The reserved tag "%s" must be present.`, + strings.Join(CheckTaglist, ",")) + errorlist := make([]*validator.FormErrorField, 0) + errorlist = append(errorlist, &validator.FormErrorField{ + ErrorField: "tags", + ErrorMsg: errMsg, + }) + err = errors.BadRequest(reason.RequestFormatError).WithMsg(errMsg) + return errorlist, err + } + } + // Check whether mandatory labels are selected + recommendExist, err := qs.tagCommon.ExistRecommend(ctx, req.Tags) if err != nil { return } - objectTagData := schema.TagChange{} - objectTagData.ObjectID = question.ID - objectTagData.Tags = req.Tags - objectTagData.UserID = req.UserID - err = qs.ChangeTag(ctx, &objectTagData) - if err != nil { - return + if !recommendExist { + errorlist := make([]*validator.FormErrorField, 0) + errorlist = append(errorlist, &validator.FormErrorField{ + ErrorField: "tags", + ErrorMsg: translator.GlobalTrans.Tr(handler.GetLangByCtx(ctx), reason.RecommendTagEnter), + }) + err = errors.BadRequest(reason.RecommendTagEnter) + return errorlist, err } + //Administrators and themselves do not need to be audited + revisionDTO := &schema.AddRevisionDTO{ UserID: question.UserID, ObjectID: question.ID, - Title: "", + Title: question.Title, Log: req.EditSummary, } - infoJSON, _ := json.Marshal(question) + + if req.NoNeedReview || req.IsAdmin || dbinfo.UserID == req.UserID { + canUpdate = true + } + + // It's not you or the administrator that needs to be reviewed + if !canUpdate { + revisionDTO.Status = entity.RevisionUnreviewedStatus + } else { + //Direct modification + revisionDTO.Status = entity.RevisionReviewPassStatus + //update question to db + saveerr := qs.questionRepo.UpdateQuestion(ctx, question, []string{"title", "original_text", "parsed_text", "updated_at", "post_update_time", "last_edit_user_id"}) + if saveerr != nil { + return questionInfo, saveerr + } + objectTagData := schema.TagChange{} + objectTagData.ObjectID = question.ID + objectTagData.Tags = req.Tags + objectTagData.UserID = req.UserID + tagerr := qs.ChangeTag(ctx, &objectTagData) + if err != nil { + return questionInfo, tagerr + } + } + + questionWithTagsRevision, err := qs.changeQuestionToRevision(ctx, question, Tags) + if err != nil { + return nil, err + } + infoJSON, _ := json.Marshal(questionWithTagsRevision) revisionDTO.Content = string(infoJSON) - err = qs.revisionService.AddRevision(ctx, revisionDTO, true) + revisionID, err := qs.revisionService.AddRevision(ctx, revisionDTO, true) if err != nil { return } + if canUpdate { + activity_queue.AddActivity(&schema.ActivityMsg{ + UserID: req.UserID, + ObjectID: question.ID, + ActivityTypeKey: constant.ActQuestionEdited, + RevisionID: revisionID, + OriginalObjectID: question.ID, + }) + } - questionInfo, err = qs.GetQuestion(ctx, question.ID, question.UserID, false) + questionInfo, err = qs.GetQuestion(ctx, question.ID, question.UserID, req.QuestionPermission) return } // GetQuestion get question one -func (qs *QuestionService) GetQuestion(ctx context.Context, id, loginUserID string, addpv bool) (resp *schema.QuestionInfo, err error) { - question, err := qs.questioncommon.Info(ctx, id, loginUserID) +func (qs *QuestionService) GetQuestion(ctx context.Context, questionID, userID string, + per schema.QuestionPermission) (resp *schema.QuestionInfo, err error) { + question, err := qs.questioncommon.Info(ctx, questionID, userID) if err != nil { return } - if addpv { - err = qs.questioncommon.UpdataPv(ctx, id) - if err != nil { - log.Error("UpdataPv", err) - } - } - - question.MemberActions = permission.GetQuestionPermission(loginUserID, question.UserID) + question.MemberActions = permission.GetQuestionPermission(ctx, userID, question.UserID, + per.CanEdit, per.CanDelete, per.CanClose) return question, nil } +// GetQuestionAndAddPV get question one +func (qs *QuestionService) GetQuestionAndAddPV(ctx context.Context, questionID, loginUserID string, + per schema.QuestionPermission) ( + resp *schema.QuestionInfo, err error) { + err = qs.questioncommon.UpdataPv(ctx, questionID) + if err != nil { + log.Error(err) + } + return qs.GetQuestion(ctx, questionID, loginUserID, per) +} + func (qs *QuestionService) ChangeTag(ctx context.Context, objectTagData *schema.TagChange) error { return qs.tagCommon.ObjectChangeTag(ctx, objectTagData) } -func (qs *QuestionService) CheckChangeTag(ctx context.Context, oldobjectTagData, objectTagData []*entity.Tag) (bool, []string) { - return qs.tagCommon.ObjectCheckChangeTag(ctx, oldobjectTagData, objectTagData) +func (qs *QuestionService) CheckChangeReservedTag(ctx context.Context, oldobjectTagData, objectTagData []*entity.Tag) (bool, []string) { + return qs.tagCommon.CheckChangeReservedTag(ctx, oldobjectTagData, objectTagData) } func (qs *QuestionService) SearchUserList(ctx context.Context, userName, order string, page, pageSize int, loginUserID string) ([]*schema.UserQuestionInfo, int64, error) { @@ -518,12 +667,12 @@ func (qs *QuestionService) SearchByTitleLike(ctx context.Context, title string, // SimilarQuestion func (qs *QuestionService) SimilarQuestion(ctx context.Context, questionID string, loginUserID string) ([]*schema.QuestionInfo, int64, error) { list := make([]*schema.QuestionInfo, 0) - questionInfo, err := qs.GetQuestion(ctx, questionID, loginUserID, false) + question, err := qs.questioncommon.Info(ctx, questionID, loginUserID) if err != nil { - return list, 0, err + return list, 0, nil } - tagNames := make([]string, 0, len(questionInfo.Tags)) - for _, tag := range questionInfo.Tags { + tagNames := make([]string, 0, len(question.Tags)) + for _, tag := range question.Tags { tagNames = append(tagNames, tag.SlugName) } search := &schema.QuestionSearch{} @@ -581,8 +730,7 @@ func (qs *QuestionService) AdminSetQuestionStatus(ctx context.Context, questionI if !exist { return errors.BadRequest(reason.QuestionNotFound) } - questionInfo.Status = setStatus - err = qs.questionRepo.UpdateQuestionStatus(ctx, questionInfo) + err = qs.questionRepo.UpdateQuestionStatus(ctx, &entity.Question{ID: questionInfo.ID, Status: setStatus}) if err != nil { return err } @@ -593,6 +741,22 @@ func (qs *QuestionService) AdminSetQuestionStatus(ctx context.Context, questionI log.Errorf("admin delete question then rank rollback error %s", err.Error()) } } + if setStatus == entity.QuestionStatusAvailable && questionInfo.Status == entity.QuestionStatusClosed { + activity_queue.AddActivity(&schema.ActivityMsg{ + UserID: questionInfo.UserID, + ObjectID: questionInfo.ID, + OriginalObjectID: questionInfo.ID, + ActivityTypeKey: constant.ActQuestionDeleted, + }) + } + if setStatus == entity.QuestionStatusClosed && questionInfo.Status != entity.QuestionStatusClosed { + activity_queue.AddActivity(&schema.ActivityMsg{ + UserID: questionInfo.UserID, + ObjectID: questionInfo.ID, + OriginalObjectID: questionInfo.ID, + ActivityTypeKey: constant.ActQuestionClosed, + }) + } msg := &schema.NotificationMsg{} msg.ObjectID = questionInfo.ID msg.Type = schema.NotificationTypeInbox @@ -688,3 +852,16 @@ func (qs *QuestionService) CmsSearchAnswerList(ctx context.Context, search *enti } return answerlist, count, nil } + +func (qs *QuestionService) changeQuestionToRevision(ctx context.Context, questionInfo *entity.Question, tags []*entity.Tag) ( + questionRevision *entity.QuestionWithTagsRevision, err error) { + questionRevision = &entity.QuestionWithTagsRevision{} + questionRevision.Question = *questionInfo + + for _, tag := range tags { + item := &entity.TagSimpleInfoForRevision{} + _ = copier.Copy(item, tag) + questionRevision.Tags = append(questionRevision.Tags, item) + } + return questionRevision, nil +} diff --git a/internal/service/rank/rank_service.go b/internal/service/rank/rank_service.go index c632149d..22bae247 100644 --- a/internal/service/rank/rank_service.go +++ b/internal/service/rank/rank_service.go @@ -3,6 +3,7 @@ package rank import ( "context" + "github.com/answerdev/answer/internal/base/constant" "github.com/answerdev/answer/internal/base/pager" "github.com/answerdev/answer/internal/base/reason" "github.com/answerdev/answer/internal/entity" @@ -17,27 +18,35 @@ import ( ) const ( - QuestionAddRank = "rank.question.add" - QuestionEditRank = "rank.question.edit" - QuestionDeleteRank = "rank.question.delete" - QuestionVoteUpRank = "rank.question.vote_up" - QuestionVoteDownRank = "rank.question.vote_down" - AnswerAddRank = "rank.answer.add" - AnswerEditRank = "rank.answer.edit" - AnswerDeleteRank = "rank.answer.delete" - AnswerAcceptRank = "rank.answer.accept" - AnswerVoteUpRank = "rank.answer.vote_up" - AnswerVoteDownRank = "rank.answer.vote_down" - CommentAddRank = "rank.comment.add" - CommentEditRank = "rank.comment.edit" - CommentDeleteRank = "rank.comment.delete" - ReportAddRank = "rank.report.add" - TagAddRank = "rank.tag.add" - TagEditRank = "rank.tag.edit" - TagDeleteRank = "rank.tag.delete" - TagSynonymRank = "rank.tag.synonym" - LinkUrlLimitRank = "rank.link.url_limit" - VoteDetailRank = "rank.vote.detail" + QuestionAddRank = "rank.question.add" + QuestionEditRank = "rank.question.edit" + QuestionEditWithoutReviewRank = "rank.question.edit_without_review" + QuestionDeleteRank = "rank.question.delete" + QuestionVoteUpRank = "rank.question.vote_up" + QuestionVoteDownRank = "rank.question.vote_down" + AnswerAddRank = "rank.answer.add" + AnswerEditRank = "rank.answer.edit" + AnswerEditWithoutReviewRank = "rank.answer.edit_without_review" + AnswerDeleteRank = "rank.answer.delete" + AnswerAcceptRank = "rank.answer.accept" + AnswerVoteUpRank = "rank.answer.vote_up" + AnswerVoteDownRank = "rank.answer.vote_down" + CommentAddRank = "rank.comment.add" + CommentEditRank = "rank.comment.edit" + CommentDeleteRank = "rank.comment.delete" + CommentVoteUpRank = "rank.comment.vote_up" + CommentVoteDownRank = "rank.comment.vote_down" + ReportAddRank = "rank.report.add" + TagAddRank = "rank.tag.add" + TagEditRank = "rank.tag.edit" + TagEditWithoutReviewRank = "rank.tag.edit_without_review" + TagDeleteRank = "rank.tag.delete" + TagSynonymRank = "rank.tag.synonym" + LinkUrlLimitRank = "rank.link.url_limit" + VoteDetailRank = "rank.vote.detail" + AnswerAuditRank = "rank.answer.audit" + QuestionAuditRank = "rank.question.audit" + TagAuditRank = "rank.tag.audit" ) type UserRankRepo interface { @@ -67,8 +76,9 @@ func NewRankService( } } -// CheckRankPermission check whether the user reputation meets the permission -func (rs *RankService) CheckRankPermission(ctx context.Context, userID string, action string) (can bool, err error) { +// CheckOperationPermission verify that the user has permission +func (rs *RankService) CheckOperationPermission(ctx context.Context, userID string, action string, objectID string) ( + can bool, err error) { if len(userID) == 0 { return false, nil } @@ -81,17 +91,130 @@ func (rs *RankService) CheckRankPermission(ctx context.Context, userID string, a if !exist { return false, nil } - currentUserRank := userInfo.Rank + // administrator have all permissions + if userInfo.IsAdmin { + return true, nil + } + if len(objectID) > 0 { + objectInfo, err := rs.objectInfoService.GetInfo(ctx, objectID) + if err != nil { + return can, err + } + // if the user is this object creator, the user can operate this object. + if objectInfo != nil && + objectInfo.ObjectCreatorUserID == userID { + return true, nil + } + } + + return rs.checkUserRank(ctx, userInfo.ID, userInfo.Rank, action) +} + +// CheckOperationPermissions verify that the user has permission +func (rs *RankService) CheckOperationPermissions(ctx context.Context, userID string, actions []string, objectID string) ( + can []bool, err error) { + can = make([]bool, len(actions)) + if len(userID) == 0 { + return can, nil + } + + // get the rank of the current user + userInfo, exist, err := rs.userCommon.GetUserBasicInfoByID(ctx, userID) + if err != nil { + return can, err + } + if !exist { + return can, nil + } + + objectOwner := false + if len(objectID) > 0 { + objectInfo, err := rs.objectInfoService.GetInfo(ctx, objectID) + if err != nil { + return can, err + } + // if the user is this object creator, the user can operate this object. + if objectInfo != nil && + objectInfo.ObjectCreatorUserID == userID { + objectOwner = true + } + } + + for idx, action := range actions { + if userInfo.IsAdmin || objectOwner { + can[idx] = true + continue + } + meetRank, err := rs.checkUserRank(ctx, userInfo.ID, userInfo.Rank, action) + if err != nil { + log.Error(err) + } + can[idx] = meetRank + } + return can, nil +} + +// CheckVotePermission verify that the user has vote permission +func (rs *RankService) CheckVotePermission(ctx context.Context, userID, objectID string, voteUp bool) ( + can bool, err error) { + if len(userID) == 0 || len(objectID) == 0 { + return false, nil + } + + // get the rank of the current user + userInfo, exist, err := rs.userCommon.GetUserBasicInfoByID(ctx, userID) + if err != nil { + return can, err + } + if !exist { + return can, nil + } + + objectInfo, err := rs.objectInfoService.GetInfo(ctx, objectID) + if err != nil { + return can, err + } + + action := "" + switch objectInfo.ObjectType { + case constant.QuestionObjectType: + if voteUp { + action = QuestionVoteUpRank + } else { + action = QuestionVoteDownRank + } + case constant.AnswerObjectType: + if voteUp { + action = AnswerVoteUpRank + } else { + action = AnswerVoteDownRank + } + case constant.CommentObjectType: + if voteUp { + action = CommentVoteUpRank + } else { + action = CommentVoteDownRank + } + } + meetRank, err := rs.checkUserRank(ctx, userInfo.ID, userInfo.Rank, action) + if err != nil { + log.Error(err) + } + return meetRank, nil +} + +// CheckRankPermission verify that the user meets the prestige criteria +func (rs *RankService) checkUserRank(ctx context.Context, userID string, userRank int, action string) ( + can bool, err error) { // get the amount of rank required for the current operation requireRank, err := rs.configRepo.GetInt(action) if err != nil { return false, err } - - if currentUserRank < requireRank { + if userRank < requireRank || requireRank < 0 { log.Debugf("user %s want to do action %s, but rank %d < %d", - userInfo.DisplayName, action, currentUserRank, requireRank) + userID, action, userRank, requireRank) return false, nil } return true, nil diff --git a/internal/service/report/report_service.go b/internal/service/report/report_service.go index 251c3239..112996bf 100644 --- a/internal/service/report/report_service.go +++ b/internal/service/report/report_service.go @@ -47,7 +47,7 @@ func (rs *ReportService) AddReport(ctx context.Context, req *schema.AddReportReq report := &entity.Report{ UserID: req.UserID, - ReportedUserID: objInfo.ObjectCreator, + ReportedUserID: objInfo.ObjectCreatorUserID, ObjectID: req.ObjectID, ObjectType: objectTypeNumber, ReportType: req.ReportType, diff --git a/internal/service/revision/revision.go b/internal/service/revision/revision.go index 1d5d999c..f368a27b 100644 --- a/internal/service/revision/revision.go +++ b/internal/service/revision/revision.go @@ -2,6 +2,7 @@ package revision import ( "context" + "github.com/answerdev/answer/internal/entity" "xorm.io/xorm" ) @@ -9,7 +10,11 @@ import ( // RevisionRepo revision repository type RevisionRepo interface { AddRevision(ctx context.Context, revision *entity.Revision, autoUpdateRevisionID bool) (err error) + GetRevisionByID(ctx context.Context, revisionID string) (revision *entity.Revision, exist bool, err error) GetLastRevisionByObjectID(ctx context.Context, objectID string) (revision *entity.Revision, exist bool, err error) GetRevisionList(ctx context.Context, revision *entity.Revision) (revisionList []entity.Revision, err error) UpdateObjectRevisionId(ctx context.Context, revision *entity.Revision, session *xorm.Session) (err error) + ExistUnreviewedByObjectID(ctx context.Context, objectID string) (revision *entity.Revision, exist bool, err error) + GetUnreviewedRevisionPage(ctx context.Context, page, pageSize int, objectTypes []int) ([]*entity.Revision, int64, error) + UpdateStatus(ctx context.Context, id string, status int) (err error) } diff --git a/internal/service/revision_common/revision_service.go b/internal/service/revision_common/revision_service.go index cee53710..cd5c067a 100644 --- a/internal/service/revision_common/revision_service.go +++ b/internal/service/revision_common/revision_service.go @@ -3,8 +3,10 @@ package revision_common import ( "context" + "github.com/answerdev/answer/internal/base/reason" "github.com/answerdev/answer/internal/service/revision" usercommon "github.com/answerdev/answer/internal/service/user_common" + "github.com/segmentfault/pacman/errors" "github.com/answerdev/answer/internal/entity" "github.com/answerdev/answer/internal/schema" @@ -24,17 +26,45 @@ func NewRevisionService(revisionRepo revision.RevisionRepo, userRepo usercommon. } } +func (rs *RevisionService) GetUnreviewedRevisionCount(ctx context.Context, req *schema.RevisionSearch) (count int64, err error) { + if len(req.GetCanReviewObjectTypes()) == 0 { + return 0, nil + } + _, count, err = rs.revisionRepo.GetUnreviewedRevisionPage(ctx, req.Page, 1, req.GetCanReviewObjectTypes()) + return count, err +} + // AddRevision add revision // // autoUpdateRevisionID bool : if autoUpdateRevisionID is true , the object.revision_id will be updated, // if not need auto update object.revision_id, it must be false. // example: user can edit the object, but need audit, the revision_id will be updated when admin approved -func (rs *RevisionService) AddRevision(ctx context.Context, req *schema.AddRevisionDTO, autoUpdateRevisionID bool) (err error) { +func (rs *RevisionService) AddRevision(ctx context.Context, req *schema.AddRevisionDTO, autoUpdateRevisionID bool) ( + revisionID string, err error) { rev := &entity.Revision{} _ = copier.Copy(rev, req) err = rs.revisionRepo.AddRevision(ctx, rev, autoUpdateRevisionID) if err != nil { - return err + return "", err } - return nil + return rev.ID, nil +} + +// GetRevision get revision +func (rs *RevisionService) GetRevision(ctx context.Context, revisionID string) ( + revision *entity.Revision, err error) { + revisionInfo, exist, err := rs.revisionRepo.GetRevisionByID(ctx, revisionID) + if err != nil { + return nil, err + } + if !exist { + return nil, errors.BadRequest(reason.ObjectNotFound) + } + return revisionInfo, nil +} + +// ExistUnreviewedByObjectID +func (rs *RevisionService) ExistUnreviewedByObjectID(ctx context.Context, objectID string) (revision *entity.Revision, exist bool, err error) { + revision, exist, err = rs.revisionRepo.ExistUnreviewedByObjectID(ctx, objectID) + return revision, exist, err } diff --git a/internal/service/revision_service.go b/internal/service/revision_service.go index fe9da647..57688014 100644 --- a/internal/service/revision_service.go +++ b/internal/service/revision_service.go @@ -3,37 +3,296 @@ package service import ( "context" "encoding/json" + "time" "github.com/answerdev/answer/internal/base/constant" + "github.com/answerdev/answer/internal/base/pager" + "github.com/answerdev/answer/internal/base/reason" "github.com/answerdev/answer/internal/entity" "github.com/answerdev/answer/internal/schema" + "github.com/answerdev/answer/internal/service/activity_queue" + answercommon "github.com/answerdev/answer/internal/service/answer_common" + "github.com/answerdev/answer/internal/service/notice_queue" + "github.com/answerdev/answer/internal/service/object_info" questioncommon "github.com/answerdev/answer/internal/service/question_common" "github.com/answerdev/answer/internal/service/revision" + "github.com/answerdev/answer/internal/service/tag_common" + tagcommon "github.com/answerdev/answer/internal/service/tag_common" usercommon "github.com/answerdev/answer/internal/service/user_common" + "github.com/answerdev/answer/pkg/converter" + "github.com/answerdev/answer/pkg/obj" "github.com/jinzhu/copier" + "github.com/segmentfault/pacman/errors" + "github.com/segmentfault/pacman/log" ) // RevisionService user service type RevisionService struct { - revisionRepo revision.RevisionRepo - userCommon *usercommon.UserCommon - questionCommon *questioncommon.QuestionCommon - answerService *AnswerService + 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 } func NewRevisionService( revisionRepo revision.RevisionRepo, userCommon *usercommon.UserCommon, questionCommon *questioncommon.QuestionCommon, - answerService *AnswerService) *RevisionService { + answerService *AnswerService, + objectInfoService *object_info.ObjService, + questionRepo questioncommon.QuestionRepo, + answerRepo answercommon.AnswerRepo, + tagRepo tag_common.TagRepo, + tagCommon *tagcommon.TagCommonService, +) *RevisionService { return &RevisionService{ - revisionRepo: revisionRepo, - userCommon: userCommon, - questionCommon: questionCommon, - answerService: answerService, + revisionRepo: revisionRepo, + userCommon: userCommon, + questionCommon: questionCommon, + answerService: answerService, + objectInfoService: objectInfoService, + questionRepo: questionRepo, + answerRepo: answerRepo, + tagRepo: tagRepo, + tagCommon: tagCommon, } } +func (rs *RevisionService) RevisionAudit(ctx context.Context, req *schema.RevisionAuditReq) (err error) { + revisioninfo, exist, err := rs.revisionRepo.GetRevisionByID(ctx, req.ID) + if err != nil { + return + } + if !exist { + return + } + if revisioninfo.Status != entity.RevisionUnreviewedStatus { + return + } + if req.Operation == schema.RevisionAuditReject { + err = rs.revisionRepo.UpdateStatus(ctx, req.ID, entity.RevisionReviewRejectStatus) + return + } + if req.Operation == schema.RevisionAuditApprove { + objectType, objectTypeerr := obj.GetObjectTypeStrByObjectID(revisioninfo.ObjectID) + if objectTypeerr != nil { + return objectTypeerr + } + revisionitem := &schema.GetRevisionResp{} + _ = copier.Copy(revisionitem, revisioninfo) + rs.parseItem(ctx, revisionitem) + var saveErr error + switch objectType { + case constant.QuestionObjectType: + if !req.CanReviewQuestion { + saveErr = errors.BadRequest(reason.RevisionNoPermission) + } else { + saveErr = rs.revisionAuditQuestion(ctx, revisionitem) + } + case constant.AnswerObjectType: + if !req.CanReviewAnswer { + saveErr = errors.BadRequest(reason.RevisionNoPermission) + } else { + saveErr = rs.revisionAuditAnswer(ctx, revisionitem) + } + case constant.TagObjectType: + if !req.CanReviewTag { + saveErr = errors.BadRequest(reason.RevisionNoPermission) + } else { + saveErr = rs.revisionAuditTag(ctx, revisionitem) + } + } + if saveErr != nil { + return saveErr + } + err = rs.revisionRepo.UpdateStatus(ctx, req.ID, entity.RevisionReviewPassStatus) + return + } + + return nil +} + +func (rs *RevisionService) revisionAuditQuestion(ctx context.Context, revisionitem *schema.GetRevisionResp) (err error) { + questioninfo, ok := revisionitem.ContentParsed.(*schema.QuestionInfo) + if ok { + now := time.Now() + question := &entity.Question{} + question.ID = questioninfo.ID + question.Title = questioninfo.Title + question.OriginalText = questioninfo.Content + question.ParsedText = questioninfo.HTML + question.UpdatedAt = now + question.PostUpdateTime = now + question.LastEditUserID = revisionitem.UserID + saveerr := rs.questionRepo.UpdateQuestion(ctx, question, []string{"title", "original_text", "parsed_text", "updated_at", "post_update_time", "last_edit_user_id"}) + if saveerr != nil { + return saveerr + } + objectTagTags := make([]*schema.TagItem, 0) + for _, tag := range questioninfo.Tags { + item := &schema.TagItem{} + item.SlugName = tag.SlugName + objectTagTags = append(objectTagTags, item) + } + objectTagData := schema.TagChange{} + objectTagData.ObjectID = question.ID + objectTagData.Tags = objectTagTags + saveerr = rs.tagCommon.ObjectChangeTag(ctx, &objectTagData) + if saveerr != nil { + return saveerr + } + activity_queue.AddActivity(&schema.ActivityMsg{ + UserID: revisionitem.UserID, + ObjectID: revisionitem.ObjectID, + ActivityTypeKey: constant.ActQuestionEdited, + RevisionID: revisionitem.ID, + OriginalObjectID: revisionitem.ObjectID, + }) + } + return nil +} + +func (rs *RevisionService) revisionAuditAnswer(ctx context.Context, revisionitem *schema.GetRevisionResp) (err error) { + answerinfo, ok := revisionitem.ContentParsed.(*schema.AnswerInfo) + if ok { + now := time.Now() + insertData := new(entity.Answer) + insertData.ID = answerinfo.ID + insertData.OriginalText = answerinfo.Content + insertData.ParsedText = answerinfo.HTML + insertData.UpdatedAt = now + insertData.LastEditUserID = revisionitem.UserID + saveerr := rs.answerRepo.UpdateAnswer(ctx, insertData, []string{"original_text", "parsed_text", "update_time", "last_edit_user_id"}) + if saveerr != nil { + return saveerr + } + saveerr = rs.questionCommon.UpdataPostTime(ctx, answerinfo.QuestionID) + if saveerr != nil { + return saveerr + } + questionInfo, exist, err := rs.questionRepo.GetQuestion(ctx, answerinfo.QuestionID) + if err != nil { + return err + } + if !exist { + return errors.BadRequest(reason.QuestionNotFound) + } + msg := &schema.NotificationMsg{ + TriggerUserID: revisionitem.UserID, + ReceiverUserID: questionInfo.UserID, + Type: schema.NotificationTypeInbox, + ObjectID: answerinfo.ID, + } + msg.ObjectType = constant.AnswerObjectType + msg.NotificationAction = constant.UpdateAnswer + notice_queue.AddNotification(msg) + + activity_queue.AddActivity(&schema.ActivityMsg{ + UserID: revisionitem.UserID, + ObjectID: insertData.ID, + OriginalObjectID: insertData.ID, + ActivityTypeKey: constant.ActAnswerEdited, + RevisionID: revisionitem.ID, + }) + } + return nil +} + +func (rs *RevisionService) revisionAuditTag(ctx context.Context, revisionitem *schema.GetRevisionResp) (err error) { + taginfo, ok := revisionitem.ContentParsed.(*schema.GetTagResp) + if ok { + tag := &entity.Tag{} + tag.ID = taginfo.TagID + tag.OriginalText = taginfo.OriginalText + tag.ParsedText = taginfo.ParsedText + saveerr := rs.tagRepo.UpdateTag(ctx, tag) + if saveerr != nil { + return saveerr + } + + tagInfo, exist, err := rs.tagCommon.GetTagByID(ctx, taginfo.TagID) + if err != nil { + return err + } + if !exist { + return errors.BadRequest(reason.TagNotFound) + } + if tagInfo.MainTagID == 0 && len(tagInfo.SlugName) > 0 { + log.Debugf("tag %s update slug_name", tagInfo.SlugName) + tagList, err := rs.tagRepo.GetTagList(ctx, &entity.Tag{MainTagID: converter.StringToInt64(tagInfo.ID)}) + if err != nil { + return err + } + updateTagSlugNames := make([]string, 0) + for _, tag := range tagList { + updateTagSlugNames = append(updateTagSlugNames, tag.SlugName) + } + err = rs.tagRepo.UpdateTagSynonym(ctx, updateTagSlugNames, converter.StringToInt64(tagInfo.ID), tagInfo.MainTagSlugName) + if err != nil { + return err + } + } + + activity_queue.AddActivity(&schema.ActivityMsg{ + UserID: revisionitem.UserID, + ObjectID: taginfo.TagID, + OriginalObjectID: taginfo.TagID, + ActivityTypeKey: constant.ActTagEdited, + RevisionID: revisionitem.ID, + }) + } + return nil +} + +// GetUnreviewedRevisionPage get unreviewed list +func (rs *RevisionService) GetUnreviewedRevisionPage(ctx context.Context, req *schema.RevisionSearch) ( + resp *pager.PageModel, err error) { + revisionResp := make([]*schema.GetUnreviewedRevisionResp, 0) + if len(req.GetCanReviewObjectTypes()) == 0 { + return pager.NewPageModel(0, revisionResp), nil + } + revisionPage, total, err := rs.revisionRepo.GetUnreviewedRevisionPage( + ctx, req.Page, 1, req.GetCanReviewObjectTypes()) + if err != nil { + return nil, err + } + for _, rev := range revisionPage { + item := &schema.GetUnreviewedRevisionResp{} + _, ok := constant.ObjectTypeNumberMapping[rev.ObjectType] + if !ok { + continue + } + item.Type = constant.ObjectTypeNumberMapping[rev.ObjectType] + info, err := rs.objectInfoService.GetUnreviewedRevisionInfo(ctx, rev.ObjectID) + if err != nil { + return nil, err + } + item.Info = info + revisionitem := &schema.GetRevisionResp{} + _ = copier.Copy(revisionitem, rev) + rs.parseItem(ctx, revisionitem) + item.UnreviewedInfo = revisionitem + + // get user info + userInfo, exists, e := rs.userCommon.GetUserBasicInfoByID(ctx, revisionitem.UserID) + if e != nil { + return nil, e + } + if exists { + var uinfo schema.UserBasicInfo + err = copier.Copy(&uinfo, userInfo) + item.UnreviewedInfo.UserInfo = uinfo + } + revisionResp = append(revisionResp, item) + } + return pager.NewPageModel(total, revisionResp), nil +} + // GetRevisionList get revision list all func (rs *RevisionService) GetRevisionList(ctx context.Context, req *schema.GetRevisionListReq) (resp []schema.GetRevisionResp, err error) { var ( @@ -75,7 +334,7 @@ func (rs *RevisionService) GetRevisionList(ctx context.Context, req *schema.GetR func (rs *RevisionService) parseItem(ctx context.Context, item *schema.GetRevisionResp) { var ( err error - question entity.Question + question entity.QuestionWithTagsRevision questionInfo *schema.QuestionInfo answer entity.Answer answerInfo *schema.AnswerInfo @@ -89,7 +348,7 @@ func (rs *RevisionService) parseItem(ctx context.Context, item *schema.GetRevisi if err != nil { break } - questionInfo = rs.questionCommon.ShowFormat(ctx, &question) + questionInfo = rs.questionCommon.ShowFormatWithTag(ctx, &question) item.ContentParsed = questionInfo case constant.ObjectTypeStrMapping["answer"]: err = json.Unmarshal([]byte(item.Content), &answer) @@ -125,3 +384,15 @@ func (rs *RevisionService) parseItem(ctx context.Context, item *schema.GetRevisi } item.CreatedAtParsed = item.CreatedAt.Unix() } + +// CheckCanUpdateRevision can check revision +func (rs *RevisionService) CheckCanUpdateRevision(ctx context.Context, req *schema.CheckCanQuestionUpdate) (err error) { + _, exist, err := rs.revisionRepo.ExistUnreviewedByObjectID(ctx, req.ID) + if err != nil { + return err + } + if exist { + return errors.BadRequest(reason.RevisionReviewUnderway) + } + return nil +} diff --git a/internal/service/search/tag.go b/internal/service/search/tag.go deleted file mode 100644 index 806880f0..00000000 --- a/internal/service/search/tag.go +++ /dev/null @@ -1,102 +0,0 @@ -package search - -import ( - "context" - "regexp" - "strings" - - "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/search_common" - "github.com/answerdev/answer/internal/service/tag_common" -) - -type TagSearch struct { - repo search_common.SearchRepo - tagCommonService *tag_common.TagCommonService - followCommon activity_common.FollowRepo - page int - size int - exp string - w string - userID string - Extra schema.GetTagPageResp - order string -} - -func NewTagSearch(repo search_common.SearchRepo, - tagCommonService *tag_common.TagCommonService, followCommon activity_common.FollowRepo) *TagSearch { - return &TagSearch{ - repo: repo, - tagCommonService: tagCommonService, - followCommon: followCommon, - } -} - -// Parse -// example: "[tag]hello" -> {exp="tag" w="hello"} -func (ts *TagSearch) Parse(dto *schema.SearchDTO) (ok bool) { - exp := "" - w := dto.Query - q := w - p := `(?m)^\[([a-zA-Z0-9-\+\.#]+)\]` - - re := regexp.MustCompile(p) - res := re.FindStringSubmatch(q) - if len(res) == 2 { - exp = res[1] - trimLen := len(res[0]) - w = q[trimLen:] - ok = true - } - w = strings.TrimSpace(w) - ts.exp = exp - ts.w = w - ts.page = dto.Page - ts.size = dto.Size - ts.userID = dto.UserID - ts.order = dto.Order - return ok -} - -func (ts *TagSearch) Search(ctx context.Context) (resp []schema.SearchResp, total int64, err error) { - var ( - words []string - tag *entity.Tag - exists, followed bool - ) - tag, exists, err = ts.tagCommonService.GetTagBySlugName(ctx, ts.exp) - if err != nil { - return - } - - if ts.userID != "" { - followed, err = ts.followCommon.IsFollowed(ts.userID, tag.ID) - } - - ts.Extra = schema.GetTagPageResp{ - TagID: tag.ID, - SlugName: tag.SlugName, - DisplayName: tag.DisplayName, - OriginalText: tag.OriginalText, - ParsedText: tag.ParsedText, - QuestionCount: tag.QuestionCount, - IsFollower: followed, - Recommend: tag.Recommend, - Reserved: tag.Reserved, - } - ts.Extra.GetExcerpt() - - if !exists { - return - } - words = strings.Split(ts.w, " ") - if len(words) > 3 { - words = words[:4] - } - - resp, total, err = ts.repo.SearchContents(ctx, words, tag.ID, "", -1, ts.page, ts.size, ts.order) - - return -} diff --git a/internal/service/tag/tag_service.go b/internal/service/tag/tag_service.go index dcf02029..9bcf89d1 100644 --- a/internal/service/tag/tag_service.go +++ b/internal/service/tag/tag_service.go @@ -4,9 +4,11 @@ import ( "context" "encoding/json" + "github.com/answerdev/answer/internal/base/constant" + "github.com/answerdev/answer/internal/service/activity_queue" "github.com/answerdev/answer/internal/service/revision_common" "github.com/answerdev/answer/internal/service/siteinfo_common" - "github.com/answerdev/answer/internal/service/tag_common" + tagcommonser "github.com/answerdev/answer/internal/service/tag_common" "github.com/answerdev/answer/pkg/htmltext" "github.com/answerdev/answer/internal/base/pager" @@ -21,17 +23,10 @@ import ( "github.com/segmentfault/pacman/log" ) -type TagRepo interface { - RemoveTag(ctx context.Context, tagID string) (err error) - UpdateTag(ctx context.Context, tag *entity.Tag) (err error) - UpdateTagSynonym(ctx context.Context, tagSlugNameList []string, mainTagID int64, mainTagSlugName string) (err error) - GetTagList(ctx context.Context, tag *entity.Tag) (tagList []*entity.Tag, err error) -} - // TagService user service type TagService struct { - tagRepo TagRepo - tagCommonService *tag_common.TagCommonService + tagRepo tagcommonser.TagRepo + tagCommonService *tagcommonser.TagCommonService revisionService *revision_common.RevisionService followCommon activity_common.FollowRepo siteInfoService *siteinfo_common.SiteInfoCommonService @@ -39,8 +34,8 @@ type TagService struct { // NewTagService new tag service func NewTagService( - tagRepo TagRepo, - tagCommonService *tag_common.TagCommonService, + tagRepo tagcommonser.TagRepo, + tagCommonService *tagcommonser.TagCommonService, revisionService *revision_common.RevisionService, followCommon activity_common.FollowRepo, siteInfoService *siteinfo_common.SiteInfoCommonService) *TagService { @@ -66,50 +61,7 @@ func (ts *TagService) RemoveTag(ctx context.Context, tagID string) (err error) { // UpdateTag update tag func (ts *TagService) UpdateTag(ctx context.Context, req *schema.UpdateTagReq) (err error) { - tag := &entity.Tag{} - _ = copier.Copy(tag, req) - tag.ID = req.TagID - err = ts.tagRepo.UpdateTag(ctx, tag) - if err != nil { - return err - } - - tagInfo, exist, err := ts.tagCommonService.GetTagByID(ctx, req.TagID) - if err != nil { - return err - } - if !exist { - return errors.BadRequest(reason.TagNotFound) - } - if tagInfo.MainTagID == 0 && len(req.SlugName) > 0 { - log.Debugf("tag %s update slug_name", tagInfo.SlugName) - tagList, err := ts.tagRepo.GetTagList(ctx, &entity.Tag{MainTagID: converter.StringToInt64(tagInfo.ID)}) - if err != nil { - return err - } - updateTagSlugNames := make([]string, 0) - for _, tag := range tagList { - updateTagSlugNames = append(updateTagSlugNames, tag.SlugName) - } - err = ts.tagRepo.UpdateTagSynonym(ctx, updateTagSlugNames, converter.StringToInt64(tagInfo.ID), tagInfo.MainTagSlugName) - if err != nil { - return err - } - } - - revisionDTO := &schema.AddRevisionDTO{ - UserID: req.UserID, - ObjectID: tag.ID, - Title: tag.SlugName, - Log: req.EditSummary, - } - tagInfoJson, _ := json.Marshal(tagInfo) - revisionDTO.Content = string(tagInfoJson) - err = ts.revisionService.AddRevision(ctx, revisionDTO, true) - if err != nil { - return err - } - return + return ts.tagCommonService.UpdateTag(ctx, req) } // GetTagInfo get tag one @@ -154,7 +106,7 @@ func (ts *TagService) GetTagInfo(ctx context.Context, req *schema.GetTagInfoReq) resp.Recommend = tagInfo.Recommend resp.Reserved = tagInfo.Reserved resp.IsFollower = ts.checkTagIsFollow(ctx, req.UserID, tagInfo.ID) - resp.MemberActions = permission.GetTagPermission(req.UserID, req.UserID) + resp.MemberActions = permission.GetTagPermission(ctx, req.CanEdit, req.CanDelete) resp.GetExcerpt() return resp, nil } @@ -198,7 +150,8 @@ func (ts *TagService) GetFollowingTags(ctx context.Context, userID string) ( // GetTagSynonyms get tag synonyms func (ts *TagService) GetTagSynonyms(ctx context.Context, req *schema.GetTagSynonymsReq) ( - resp []*schema.GetTagSynonymsResp, err error) { + resp *schema.GetTagSynonymsResp, err error) { + resp = &schema.GetTagSynonymsResp{Synonyms: make([]*schema.TagSynonym, 0)} tag, exist, err := ts.tagCommonService.GetTagByID(ctx, req.TagID) if err != nil { return @@ -230,15 +183,15 @@ func (ts *TagService) GetTagSynonyms(ctx context.Context, req *schema.GetTagSyno mainTagSlugName = tag.SlugName } - resp = make([]*schema.GetTagSynonymsResp, 0) for _, t := range tagList { - resp = append(resp, &schema.GetTagSynonymsResp{ + resp.Synonyms = append(resp.Synonyms, &schema.TagSynonym{ TagID: t.ID, SlugName: t.SlugName, DisplayName: t.DisplayName, MainTagSlugName: mainTagSlugName, }) } + resp.MemberActions = permission.GetTagSynonymPermission(ctx, req.CanEdit) return } @@ -281,6 +234,7 @@ func (ts *TagService) UpdateTagSynonym(ctx context.Context, req *schema.UpdateTa item.OriginalText = tag.OriginalText item.ParsedText = tag.ParsedText item.Status = entity.TagStatusAvailable + item.UserID = req.UserID needAddTagList = append(needAddTagList, item) } @@ -299,10 +253,17 @@ func (ts *TagService) UpdateTagSynonym(ctx context.Context, req *schema.UpdateTa } tagInfoJson, _ := json.Marshal(tag) revisionDTO.Content = string(tagInfoJson) - err = ts.revisionService.AddRevision(ctx, revisionDTO, true) + revisionID, err := ts.revisionService.AddRevision(ctx, revisionDTO, true) if err != nil { return err } + activity_queue.AddActivity(&schema.ActivityMsg{ + UserID: req.UserID, + ObjectID: tag.ID, + OriginalObjectID: tag.ID, + ActivityTypeKey: constant.ActTagCreated, + RevisionID: revisionID, + }) } } @@ -339,6 +300,7 @@ func (ts *TagService) UpdateTagSynonym(ctx context.Context, req *schema.UpdateTa func (ts *TagService) GetTagWithPage(ctx context.Context, req *schema.GetTagWithPageReq) (pageModel *pager.PageModel, err error) { tag := &entity.Tag{} _ = copier.Copy(tag, req) + tag.UserID = "" page := req.Page pageSize := req.PageSize diff --git a/internal/service/tag_common/tag_common.go b/internal/service/tag_common/tag_common.go index 42a156a7..d9d97423 100644 --- a/internal/service/tag_common/tag_common.go +++ b/internal/service/tag_common/tag_common.go @@ -7,12 +7,15 @@ import ( "sort" "strings" + "github.com/answerdev/answer/internal/base/constant" "github.com/answerdev/answer/internal/base/reason" "github.com/answerdev/answer/internal/base/validator" "github.com/answerdev/answer/internal/entity" "github.com/answerdev/answer/internal/schema" + "github.com/answerdev/answer/internal/service/activity_queue" "github.com/answerdev/answer/internal/service/revision_common" "github.com/answerdev/answer/internal/service/siteinfo_common" + "github.com/answerdev/answer/pkg/converter" "github.com/segmentfault/pacman/errors" "github.com/segmentfault/pacman/log" ) @@ -31,6 +34,13 @@ type TagCommonRepo interface { UpdateTagQuestionCount(ctx context.Context, tagID string, questionCount int) (err error) } +type TagRepo interface { + RemoveTag(ctx context.Context, tagID string) (err error) + UpdateTag(ctx context.Context, tag *entity.Tag) (err error) + UpdateTagSynonym(ctx context.Context, tagSlugNameList []string, mainTagID int64, mainTagSlugName string) (err error) + GetTagList(ctx context.Context, tag *entity.Tag) (tagList []*entity.Tag, err error) +} + type TagRelRepo interface { AddTagRelList(ctx context.Context, tagList []*entity.TagRel) (err error) RemoveTagRelListByIDs(ctx context.Context, ids []int64) (err error) @@ -46,17 +56,22 @@ type TagCommonService struct { revisionService *revision_common.RevisionService tagCommonRepo TagCommonRepo tagRelRepo TagRelRepo + tagRepo TagRepo siteInfoService *siteinfo_common.SiteInfoCommonService } // NewTagCommonService new tag service -func NewTagCommonService(tagCommonRepo TagCommonRepo, tagRelRepo TagRelRepo, +func NewTagCommonService( + tagCommonRepo TagCommonRepo, + tagRelRepo TagRelRepo, + tagRepo TagRepo, revisionService *revision_common.RevisionService, siteInfoService *siteinfo_common.SiteInfoCommonService, ) *TagCommonService { return &TagCommonService{ tagCommonRepo: tagCommonRepo, tagRelRepo: tagRelRepo, + tagRepo: tagRepo, revisionService: revisionService, siteInfoService: siteInfoService, } @@ -68,7 +83,7 @@ func (ts *TagCommonService) SearchTagLike(ctx context.Context, req *schema.Searc if err != nil { return } - ts.tagsFormatRecommendAndReserved(ctx, tags) + ts.TagsFormatRecommendAndReserved(ctx, tags) for _, tag := range tags { item := schema.SearchTagLikeResp{} item.SlugName = tag.SlugName @@ -166,7 +181,7 @@ func (ts *TagCommonService) GetTagListByNames(ctx context.Context, tagNames []st if err != nil { return nil, err } - ts.tagsFormatRecommendAndReserved(ctx, tagList) + ts.TagsFormatRecommendAndReserved(ctx, tagList) return tagList, nil } @@ -231,7 +246,7 @@ func (ts *TagCommonService) GetTagListByIDs(ctx context.Context, ids []string) ( if err != nil { return nil, err } - ts.tagsFormatRecommendAndReserved(ctx, tagList) + ts.TagsFormatRecommendAndReserved(ctx, tagList) return } @@ -242,7 +257,7 @@ func (ts *TagCommonService) GetTagPage(ctx context.Context, page, pageSize int, if err != nil { return nil, 0, err } - ts.tagsFormatRecommendAndReserved(ctx, tagList) + ts.TagsFormatRecommendAndReserved(ctx, tagList) return } @@ -276,7 +291,7 @@ func (ts *TagCommonService) TagFormat(ctx context.Context, tags []*entity.Tag) ( return objTags, nil } -func (ts *TagCommonService) tagsFormatRecommendAndReserved(ctx context.Context, tagList []*entity.Tag) { +func (ts *TagCommonService) TagsFormatRecommendAndReserved(ctx context.Context, tagList []*entity.Tag) { if len(tagList) == 0 { return } @@ -389,6 +404,7 @@ func (ts *TagCommonService) CheckTag(ctx context.Context, tags []string, userID item.OriginalText = "" item.ParsedText = "" item.Status = entity.TagStatusAvailable + item.UserID = userID addTagList = append(addTagList, item) addTagMsgList = append(addTagMsgList, tag) } @@ -403,7 +419,31 @@ func (ts *TagCommonService) CheckTag(ctx context.Context, tags []string, userID return nil } -func (ts *TagCommonService) ObjectCheckChangeTag(ctx context.Context, oldobjectTagData, objectTagData []*entity.Tag) (bool, []string) { +// CheckTagsIsChange +func (ts *TagCommonService) CheckTagsIsChange(ctx context.Context, tagNameList, oldtagNameList []string) bool { + check := make(map[string]bool) + if len(tagNameList) != len(oldtagNameList) { + return true + } + for _, item := range tagNameList { + check[item] = false + } + for _, item := range oldtagNameList { + _, ok := check[item] + if !ok { + return true + } + check[item] = true + } + for _, value := range check { + if value == false { + return true + } + } + return false +} + +func (ts *TagCommonService) CheckChangeReservedTag(ctx context.Context, oldobjectTagData, objectTagData []*entity.Tag) (bool, []string) { reservedTagsMap := make(map[string]bool) needTagsMap := make([]string, 0) for _, tag := range objectTagData { @@ -463,6 +503,7 @@ func (ts *TagCommonService) ObjectChangeTag(ctx context.Context, objectTagData * item.OriginalText = tag.OriginalText item.ParsedText = tag.ParsedText item.Status = entity.TagStatusAvailable + item.UserID = objectTagData.UserID addTagList = append(addTagList, item) } @@ -480,10 +521,17 @@ func (ts *TagCommonService) ObjectChangeTag(ctx context.Context, objectTagData * } tagInfoJson, _ := json.Marshal(tag) revisionDTO.Content = string(tagInfoJson) - err = ts.revisionService.AddRevision(ctx, revisionDTO, true) + revisionID, err := ts.revisionService.AddRevision(ctx, revisionDTO, true) if err != nil { return err } + activity_queue.AddActivity(&schema.ActivityMsg{ + UserID: objectTagData.UserID, + ObjectID: tag.ID, + OriginalObjectID: tag.ID, + ActivityTypeKey: constant.ActTagCreated, + RevisionID: revisionID, + }) } } @@ -573,3 +621,83 @@ func (ts *TagCommonService) CreateOrUpdateTagRelList(ctx context.Context, object } return nil } + +func (ts *TagCommonService) UpdateTag(ctx context.Context, req *schema.UpdateTagReq) (err error) { + var canUpdate bool + _, existUnreviewed, err := ts.revisionService.ExistUnreviewedByObjectID(ctx, req.TagID) + if err != nil { + return err + } + if existUnreviewed { + err = errors.BadRequest(reason.AnswerCannotUpdate) + return err + } + + tagInfo, exist, err := ts.GetTagByID(ctx, req.TagID) + if err != nil { + return err + } + if !exist { + return errors.BadRequest(reason.TagNotFound) + } + //If the content is the same, ignore it + if tagInfo.OriginalText == req.OriginalText { + return nil + } + + tagInfo.SlugName = req.SlugName + tagInfo.DisplayName = req.DisplayName + tagInfo.OriginalText = req.OriginalText + tagInfo.ParsedText = req.ParsedText + + revisionDTO := &schema.AddRevisionDTO{ + UserID: req.UserID, + ObjectID: tagInfo.ID, + Title: tagInfo.SlugName, + Log: req.EditSummary, + } + + if req.NoNeedReview { + canUpdate = true + err = ts.tagRepo.UpdateTag(ctx, tagInfo) + if err != nil { + return err + } + if tagInfo.MainTagID == 0 && len(req.SlugName) > 0 { + log.Debugf("tag %s update slug_name", tagInfo.SlugName) + tagList, err := ts.tagRepo.GetTagList(ctx, &entity.Tag{MainTagID: converter.StringToInt64(tagInfo.ID)}) + if err != nil { + return err + } + updateTagSlugNames := make([]string, 0) + for _, tag := range tagList { + updateTagSlugNames = append(updateTagSlugNames, tag.SlugName) + } + err = ts.tagRepo.UpdateTagSynonym(ctx, updateTagSlugNames, converter.StringToInt64(tagInfo.ID), tagInfo.MainTagSlugName) + if err != nil { + return err + } + } + revisionDTO.Status = entity.RevisionReviewPassStatus + } else { + revisionDTO.Status = entity.RevisionUnreviewedStatus + } + + tagInfoJson, _ := json.Marshal(tagInfo) + revisionDTO.Content = string(tagInfoJson) + revisionID, err := ts.revisionService.AddRevision(ctx, revisionDTO, true) + if err != nil { + return err + } + if canUpdate { + activity_queue.AddActivity(&schema.ActivityMsg{ + UserID: req.UserID, + ObjectID: tagInfo.ID, + OriginalObjectID: tagInfo.ID, + ActivityTypeKey: constant.ActTagEdited, + RevisionID: revisionID, + }) + } + + return +} diff --git a/internal/service/user_common/user.go b/internal/service/user_common/user.go index a3ea6a48..ffcf8206 100644 --- a/internal/service/user_common/user.go +++ b/internal/service/user_common/user.go @@ -38,12 +38,13 @@ func NewUserCommon(userRepo UserRepo) *UserCommon { } } -func (us *UserCommon) GetUserBasicInfoByID(ctx context.Context, ID string) (*schema.UserBasicInfo, bool, error) { +func (us *UserCommon) GetUserBasicInfoByID(ctx context.Context, ID string) ( + userBasicInfo *schema.UserBasicInfo, exist bool, err error) { userInfo, exist, err := us.userRepo.GetByUserID(ctx, ID) if err != nil { return nil, exist, err } - info := us.UserBasicInfoFormat(ctx, userInfo) + info := us.FormatUserBasicInfo(ctx, userInfo) return info, exist, nil } @@ -52,7 +53,7 @@ func (us *UserCommon) GetUserBasicInfoByUserName(ctx context.Context, username s if err != nil { return nil, exist, err } - info := us.UserBasicInfoFormat(ctx, userInfo) + info := us.FormatUserBasicInfo(ctx, userInfo) return info, exist, nil } @@ -71,21 +72,21 @@ func (us *UserCommon) BatchUserBasicInfoByID(ctx context.Context, IDs []string) return userMap, err } for _, item := range dbInfo { - info := us.UserBasicInfoFormat(ctx, item) + info := us.FormatUserBasicInfo(ctx, item) userMap[item.ID] = info } return userMap, nil } -// UserBasicInfoFormat -func (us *UserCommon) UserBasicInfoFormat(ctx context.Context, userInfo *entity.User) *schema.UserBasicInfo { +// FormatUserBasicInfo format user basic info +func (us *UserCommon) FormatUserBasicInfo(ctx context.Context, userInfo *entity.User) *schema.UserBasicInfo { userBasicInfo := &schema.UserBasicInfo{} - Avatar := schema.FormatAvatarInfo(userInfo.Avatar) userBasicInfo.ID = userInfo.ID + userBasicInfo.IsAdmin = userInfo.IsAdmin userBasicInfo.Username = userInfo.Username userBasicInfo.Rank = userInfo.Rank userBasicInfo.DisplayName = userInfo.DisplayName - userBasicInfo.Avatar = Avatar + userBasicInfo.Avatar = schema.FormatAvatarInfo(userInfo.Avatar) userBasicInfo.Website = userInfo.Website userBasicInfo.Location = userInfo.Location userBasicInfo.IPInfo = userInfo.IPInfo diff --git a/ui/src/router/routes.ts b/ui/src/router/routes.ts index be7e61ac..7c56db23 100644 --- a/ui/src/router/routes.ts +++ b/ui/src/router/routes.ts @@ -160,20 +160,10 @@ const routes: RouteNode[] = [ { path: 'users/password-reset', page: 'pages/Users/PasswordReset', - guard: async () => { - return guard.activated(); - }, }, { path: 'users/account-activation', page: 'pages/Users/ActiveEmail', - // guard: async () => { - // const notActivated = guard.notActivated(); - // if (notActivated.ok) { - // return notActivated; - // } - // return guard.notLogged(); - // }, }, { path: 'users/account-activation/success',