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',