This is an automated email from the ASF dual-hosted git repository.

kumfo pushed a commit to branch feat/1.8.0/menu
in repository https://gitbox.apache.org/repos/asf/answer.git

commit 29ec29bde770542efe2a0f299f00d7466f4fb8c9
Author: kumfo <[email protected]>
AuthorDate: Tue Jan 20 14:54:52 2026 +0800

    feat(menu): update admin menu settings to include questions, tags, and 
advanced options
---
 cmd/wire_gen.go                                    |  74 ++++-----
 go.mod                                             |   2 +-
 internal/base/constant/site_type.go                |   3 +
 internal/controller/answer_controller.go           |   2 +-
 internal/controller/siteinfo_controller.go         |  10 +-
 internal/controller_admin/siteinfo_controller.go   |  94 +++++++++--
 internal/migrations/migrations.go                  |   1 +
 internal/migrations/v30.go                         | 173 +++++++++++++++++++++
 internal/router/answer_api_router.go               |  10 +-
 internal/schema/siteinfo_schema.go                 |  41 ++++-
 internal/service/mock/siteinfo_repo_mock.go        |  81 +++++++---
 internal/service/question_common/question.go       |   2 +-
 internal/service/siteinfo/siteinfo_service.go      |  51 ++++--
 .../service/siteinfo_common/siteinfo_service.go    |  30 ++++
 internal/service/tag_common/tag_common.go          |   8 +-
 internal/service/uploader/upload.go                |  32 ++--
 16 files changed, 490 insertions(+), 124 deletions(-)

diff --git a/cmd/wire_gen.go b/cmd/wire_gen.go
index 22a70f29..2808619b 100644
--- a/cmd/wire_gen.go
+++ b/cmd/wire_gen.go
@@ -1,28 +1,8 @@
-//go:build !wireinject
-// +build !wireinject
-
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
 // Code generated by Wire. DO NOT EDIT.
 
-//go:generate go run github.com/google/wire/cmd/wire
+//go:generate go run -mod=mod github.com/google/wire/cmd/wire
+//go:build !wireinject
+// +build !wireinject
 
 package answercmd
 
@@ -172,29 +152,29 @@ func initApplication(debug bool, serverConf *conf.Server, 
dbConf *data.Database,
        tagRepo := tag.NewTagRepo(dataData, uniqueIDRepo)
        revisionRepo := revision.NewRevisionRepo(dataData, uniqueIDRepo)
        revisionService := revision_common.NewRevisionService(revisionRepo, 
userRepo)
-       v := activityqueue.NewService()
-       tagCommonService := tag_common2.NewTagCommonService(tagCommonRepo, 
tagRelRepo, tagRepo, revisionService, siteInfoCommonService, v)
+       service := activityqueue.NewService()
+       tagCommonService := tag_common2.NewTagCommonService(tagCommonRepo, 
tagRelRepo, tagRepo, revisionService, siteInfoCommonService, service)
        collectionRepo := collection.NewCollectionRepo(dataData, uniqueIDRepo)
        collectionCommon := collectioncommon.NewCollectionCommon(collectionRepo)
        answerCommon := answercommon.NewAnswerCommon(answerRepo)
        metaRepo := meta.NewMetaRepo(dataData)
        metaCommonService := metacommon.NewMetaCommonService(metaRepo)
-       questionCommon := questioncommon.NewQuestionCommon(questionRepo, 
answerRepo, voteRepo, followRepo, tagCommonService, userCommon, 
collectionCommon, answerCommon, metaCommonService, configService, v, 
revisionRepo, siteInfoCommonService, dataData)
-       v2 := eventqueue.NewService()
+       questionCommon := questioncommon.NewQuestionCommon(questionRepo, 
answerRepo, voteRepo, followRepo, tagCommonService, userCommon, 
collectionCommon, answerCommon, metaCommonService, configService, service, 
revisionRepo, siteInfoCommonService, dataData)
+       eventqueueService := eventqueue.NewService()
        fileRecordRepo := file_record.NewFileRecordRepo(dataData)
        fileRecordService := file_record2.NewFileRecordService(fileRecordRepo, 
revisionRepo, serviceConf, siteInfoCommonService, userCommon)
-       userService := content.NewUserService(userRepo, userActiveActivityRepo, 
activityRepo, emailService, authService, siteInfoCommonService, 
userRoleRelService, userCommon, userExternalLoginService, 
userNotificationConfigRepo, userNotificationConfigService, questionCommon, v2, 
fileRecordService)
+       userService := content.NewUserService(userRepo, userActiveActivityRepo, 
activityRepo, emailService, authService, siteInfoCommonService, 
userRoleRelService, userCommon, userExternalLoginService, 
userNotificationConfigRepo, userNotificationConfigService, questionCommon, 
eventqueueService, fileRecordService)
        captchaRepo := captcha.NewCaptchaRepo(dataData)
        captchaService := action.NewCaptchaService(captchaRepo)
        userController := controller.NewUserController(authService, 
userService, captchaService, emailService, siteInfoCommonService, 
userNotificationConfigService)
        commentRepo := comment.NewCommentRepo(dataData, uniqueIDRepo)
        commentCommonRepo := comment.NewCommentCommonRepo(dataData, 
uniqueIDRepo)
        objService := object_info.NewObjService(answerRepo, questionRepo, 
commentCommonRepo, tagCommonRepo, tagCommonService)
-       v3 := noticequeue.NewService()
-       v4 := noticequeue.NewExternalService()
+       noticequeueService := noticequeue.NewService()
+       externalService := noticequeue.NewExternalService()
        reviewRepo := review.NewReviewRepo(dataData)
-       reviewService := review2.NewReviewService(reviewRepo, objService, 
userCommon, userRepo, questionRepo, answerRepo, userRoleRelService, v4, 
tagCommonService, questionCommon, v3, siteInfoCommonService, commentCommonRepo)
-       commentService := comment2.NewCommentService(commentRepo, 
commentCommonRepo, userCommon, objService, voteRepo, emailService, userRepo, 
v3, v4, v, v2, reviewService)
+       reviewService := review2.NewReviewService(reviewRepo, objService, 
userCommon, userRepo, questionRepo, answerRepo, userRoleRelService, 
externalService, tagCommonService, questionCommon, noticequeueService, 
siteInfoCommonService, commentCommonRepo)
+       commentService := comment2.NewCommentService(commentRepo, 
commentCommonRepo, userCommon, objService, voteRepo, emailService, userRepo, 
noticequeueService, externalService, service, eventqueueService, reviewService)
        rolePowerRelRepo := role.NewRolePowerRelRepo(dataData)
        rolePowerRelService := role2.NewRolePowerRelService(rolePowerRelRepo, 
userRoleRelService)
        rankService := rank2.NewRankService(userCommon, userRankRepo, 
objService, userRoleRelService, rolePowerRelService, configService)
@@ -202,17 +182,17 @@ func initApplication(debug bool, serverConf *conf.Server, 
dbConf *data.Database,
        rateLimitMiddleware := middleware.NewRateLimitMiddleware(limitRepo)
        commentController := controller.NewCommentController(commentService, 
rankService, captchaService, rateLimitMiddleware)
        reportRepo := report.NewReportRepo(dataData, uniqueIDRepo)
-       tagService := tag2.NewTagService(tagRepo, tagCommonService, 
revisionService, followRepo, siteInfoCommonService, v)
-       answerActivityRepo := activity.NewAnswerActivityRepo(dataData, 
activityRepo, userRankRepo, v3)
+       tagService := tag2.NewTagService(tagRepo, tagCommonService, 
revisionService, followRepo, siteInfoCommonService, service)
+       answerActivityRepo := activity.NewAnswerActivityRepo(dataData, 
activityRepo, userRankRepo, noticequeueService)
        answerActivityService := 
activity2.NewAnswerActivityService(answerActivityRepo, configService)
-       externalNotificationService := 
notification.NewExternalNotificationService(dataData, 
userNotificationConfigRepo, followRepo, emailService, userRepo, v4, 
userExternalLoginRepo, siteInfoCommonService)
-       questionService := content.NewQuestionService(activityRepo, 
questionRepo, answerRepo, tagCommonService, tagService, questionCommon, 
userCommon, userRepo, userRoleRelService, revisionService, metaCommonService, 
collectionCommon, answerActivityService, emailService, v3, v4, v, 
siteInfoCommonService, externalNotificationService, reviewService, 
configService, v2, reviewRepo)
-       answerService := content.NewAnswerService(answerRepo, questionRepo, 
questionCommon, userCommon, collectionCommon, userRepo, revisionService, 
answerActivityService, answerCommon, voteRepo, emailService, 
userRoleRelService, v3, v4, v, reviewService, v2)
+       externalNotificationService := 
notification.NewExternalNotificationService(dataData, 
userNotificationConfigRepo, followRepo, emailService, userRepo, 
externalService, userExternalLoginRepo, siteInfoCommonService)
+       questionService := content.NewQuestionService(activityRepo, 
questionRepo, answerRepo, tagCommonService, tagService, questionCommon, 
userCommon, userRepo, userRoleRelService, revisionService, metaCommonService, 
collectionCommon, answerActivityService, emailService, noticequeueService, 
externalService, service, siteInfoCommonService, externalNotificationService, 
reviewService, configService, eventqueueService, reviewRepo)
+       answerService := content.NewAnswerService(answerRepo, questionRepo, 
questionCommon, userCommon, collectionCommon, userRepo, revisionService, 
answerActivityService, answerCommon, voteRepo, emailService, 
userRoleRelService, noticequeueService, externalService, service, 
reviewService, eventqueueService)
        reportHandle := report_handle.NewReportHandle(questionService, 
answerService, commentService)
-       reportService := report2.NewReportService(reportRepo, objService, 
userCommon, answerRepo, questionRepo, commentCommonRepo, reportHandle, 
configService, v2)
+       reportService := report2.NewReportService(reportRepo, objService, 
userCommon, answerRepo, questionRepo, commentCommonRepo, reportHandle, 
configService, eventqueueService)
        reportController := controller.NewReportController(reportService, 
rankService, captchaService)
-       contentVoteRepo := activity.NewVoteRepo(dataData, activityRepo, 
userRankRepo, v3)
-       voteService := content.NewVoteService(contentVoteRepo, configService, 
questionRepo, answerRepo, commentCommonRepo, objService, v2)
+       contentVoteRepo := activity.NewVoteRepo(dataData, activityRepo, 
userRankRepo, noticequeueService)
+       voteService := content.NewVoteService(contentVoteRepo, configService, 
questionRepo, answerRepo, commentCommonRepo, objService, eventqueueService)
        voteController := controller.NewVoteController(voteService, 
rankService, captchaService)
        tagController := controller.NewTagController(tagService, 
tagCommonService, rankService)
        followFollowRepo := activity.NewFollowRepo(dataData, uniqueIDRepo, 
activityRepo)
@@ -228,7 +208,7 @@ func initApplication(debug bool, serverConf *conf.Server, 
dbConf *data.Database,
        searchService := content.NewSearchService(searchParser, searchRepo)
        searchController := controller.NewSearchController(searchService, 
captchaService)
        reviewActivityRepo := activity.NewReviewActivityRepo(dataData, 
activityRepo, userRankRepo, configService)
-       contentRevisionService := content.NewRevisionService(revisionRepo, 
userCommon, questionCommon, answerService, objService, questionRepo, 
answerRepo, tagRepo, tagCommonService, v3, v, reportRepo, reviewService, 
reviewActivityRepo)
+       contentRevisionService := content.NewRevisionService(revisionRepo, 
userCommon, questionCommon, answerService, objService, questionRepo, 
answerRepo, tagRepo, tagCommonService, noticequeueService, service, reportRepo, 
reviewService, reviewActivityRepo)
        revisionController := 
controller.NewRevisionController(contentRevisionService, rankService)
        rankController := controller.NewRankController(rankService)
        userAdminRepo := user.NewUserAdminRepo(dataData, authRepo)
@@ -244,7 +224,7 @@ func initApplication(debug bool, serverConf *conf.Server, 
dbConf *data.Database,
        siteInfoService := siteinfo.NewSiteInfoService(siteInfoRepo, 
siteInfoCommonService, emailService, tagCommonService, configService, 
questionCommon, fileRecordService)
        siteInfoController := 
controller_admin.NewSiteInfoController(siteInfoService)
        controllerSiteInfoController := 
controller.NewSiteInfoController(siteInfoCommonService)
-       notificationCommon := 
notificationcommon.NewNotificationCommon(dataData, notificationRepo, 
userCommon, activityRepo, followRepo, objService, v3, userExternalLoginRepo, 
siteInfoCommonService)
+       notificationCommon := 
notificationcommon.NewNotificationCommon(dataData, notificationRepo, 
userCommon, activityRepo, followRepo, objService, noticequeueService, 
userExternalLoginRepo, siteInfoCommonService)
        badgeRepo := badge.NewBadgeRepo(dataData, uniqueIDRepo)
        notificationService := notification.NewNotificationService(dataData, 
notificationRepo, notificationCommon, revisionService, userRepo, reportRepo, 
reviewService, badgeRepo)
        notificationController := 
controller.NewNotificationController(notificationService, rankService)
@@ -253,7 +233,7 @@ func initApplication(debug bool, serverConf *conf.Server, 
dbConf *data.Database,
        uploaderService := uploader.NewUploaderService(serviceConf, 
siteInfoCommonService, fileRecordService)
        uploadController := controller.NewUploadController(uploaderService)
        activityActivityRepo := activity.NewActivityRepo(dataData, 
configService)
-       activityCommon := activity_common2.NewActivityCommon(activityRepo, v)
+       activityCommon := activity_common2.NewActivityCommon(activityRepo, 
service)
        commentCommonService := 
comment_common.NewCommentCommonService(commentCommonRepo)
        activityService := activity2.NewActivityService(activityActivityRepo, 
userCommon, activityCommon, tagCommonService, objService, commentCommonService, 
revisionService, metaCommonService, configService)
        activityController := controller.NewActivityController(activityService)
@@ -265,12 +245,12 @@ func initApplication(debug bool, serverConf *conf.Server, 
dbConf *data.Database,
        permissionController := controller.NewPermissionController(rankService)
        userPluginController := 
controller.NewUserPluginController(pluginCommonService)
        reviewController := controller.NewReviewController(reviewService, 
rankService, captchaService)
-       metaService := meta2.NewMetaService(metaCommonService, userCommon, 
answerRepo, questionRepo, v2)
+       metaService := meta2.NewMetaService(metaCommonService, userCommon, 
answerRepo, questionRepo, eventqueueService)
        metaController := controller.NewMetaController(metaService)
        badgeGroupRepo := badge_group.NewBadgeGroupRepo(dataData, uniqueIDRepo)
        eventRuleRepo := badge.NewEventRuleRepo(dataData)
-       badgeAwardService := badge2.NewBadgeAwardService(badgeAwardRepo, 
badgeRepo, userCommon, objService, v3)
-       badgeEventService := badge2.NewBadgeEventService(dataData, v2, 
badgeRepo, eventRuleRepo, badgeAwardService)
+       badgeAwardService := badge2.NewBadgeAwardService(badgeAwardRepo, 
badgeRepo, userCommon, objService, noticequeueService)
+       badgeEventService := badge2.NewBadgeEventService(dataData, 
eventqueueService, badgeRepo, eventRuleRepo, badgeAwardService)
        badgeService := badge2.NewBadgeService(badgeRepo, badgeGroupRepo, 
badgeAwardRepo, badgeEventService, siteInfoCommonService)
        badgeController := controller.NewBadgeController(badgeService, 
badgeAwardService)
        controller_adminBadgeController := 
controller_admin.NewBadgeController(badgeService)
@@ -281,7 +261,7 @@ func initApplication(debug bool, serverConf *conf.Server, 
dbConf *data.Database,
        avatarMiddleware := middleware.NewAvatarMiddleware(serviceConf, 
uploaderService)
        shortIDMiddleware := 
middleware.NewShortIDMiddleware(siteInfoCommonService)
        templateRenderController := 
templaterender.NewTemplateRenderController(questionService, userService, 
tagService, answerService, commentService, siteInfoCommonService, questionRepo)
-       templateController := 
controller.NewTemplateController(templateRenderController, 
siteInfoCommonService, v2, userService, questionService)
+       templateController := 
controller.NewTemplateController(templateRenderController, 
siteInfoCommonService, eventqueueService, userService, questionService)
        templateRouter := router.NewTemplateRouter(templateController, 
templateRenderController, siteInfoController, authUserMiddleware)
        connectorController := 
controller.NewConnectorController(siteInfoCommonService, emailService, 
userExternalLoginService)
        userCenterLoginService := 
user_external_login2.NewUserCenterLoginService(userRepo, userCommon, 
userExternalLoginRepo, userActiveActivityRepo, siteInfoCommonService)
diff --git a/go.mod b/go.mod
index 52d64c73..d2430295 100644
--- a/go.mod
+++ b/go.mod
@@ -37,6 +37,7 @@ require (
        github.com/grokify/html-strip-tags-go v0.1.0
        github.com/jinzhu/copier v0.4.0
        github.com/jinzhu/now v1.1.5
+       github.com/joho/godotenv v1.5.1
        github.com/lib/pq v1.10.9
        github.com/microcosm-cc/bluemonday v1.0.27
        github.com/mozillazg/go-pinyin v0.20.0
@@ -117,7 +118,6 @@ require (
        github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
        github.com/hashicorp/hcl v1.0.0 // indirect
        github.com/inconshreveable/mousetrap v1.1.0 // indirect
-       github.com/joho/godotenv v1.5.1 // indirect
        github.com/josharian/intern v1.0.0 // indirect
        github.com/json-iterator/go v1.1.12 // indirect
        github.com/klauspost/cpuid/v2 v2.2.8 // indirect
diff --git a/internal/base/constant/site_type.go 
b/internal/base/constant/site_type.go
index 65b487c5..6106b190 100644
--- a/internal/base/constant/site_type.go
+++ b/internal/base/constant/site_type.go
@@ -31,4 +31,7 @@ const (
        SiteTypeTheme         = "theme"
        SiteTypePrivileges    = "privileges"
        SiteTypeUsers         = "users"
+       SiteTypeAdvanced      = "advanced"
+       SiteTypeQuestions     = "questions"
+       SiteTypeTags          = "tags"
 )
diff --git a/internal/controller/answer_controller.go 
b/internal/controller/answer_controller.go
index e76b02cc..99c89b77 100644
--- a/internal/controller/answer_controller.go
+++ b/internal/controller/answer_controller.go
@@ -242,7 +242,7 @@ func (ac *AnswerController) AddAnswer(ctx *gin.Context) {
                return
        }
 
-       write, err := ac.siteInfoCommonService.GetSiteWrite(ctx)
+       write, err := ac.siteInfoCommonService.GetSiteQuestion(ctx)
        if err != nil {
                handler.HandleResponse(ctx, err, nil)
                return
diff --git a/internal/controller/siteinfo_controller.go 
b/internal/controller/siteinfo_controller.go
index a336fb24..8035275a 100644
--- a/internal/controller/siteinfo_controller.go
+++ b/internal/controller/siteinfo_controller.go
@@ -87,7 +87,15 @@ func (sc *SiteInfoController) GetSiteInfo(ctx *gin.Context) {
        if err != nil {
                log.Error(err)
        }
-       resp.Write, err = sc.siteInfoService.GetSiteWrite(ctx)
+       resp.Questions, err = sc.siteInfoService.GetSiteQuestion(ctx)
+       if err != nil {
+               log.Error(err)
+       }
+       resp.Tags, err = sc.siteInfoService.GetSiteTag(ctx)
+       if err != nil {
+               log.Error(err)
+       }
+       resp.Advanced, err = sc.siteInfoService.GetSiteAdvanced(ctx)
        if err != nil {
                log.Error(err)
        }
diff --git a/internal/controller_admin/siteinfo_controller.go 
b/internal/controller_admin/siteinfo_controller.go
index 8a92daba..bbab9794 100644
--- a/internal/controller_admin/siteinfo_controller.go
+++ b/internal/controller_admin/siteinfo_controller.go
@@ -82,16 +82,42 @@ func (sc *SiteInfoController) GetSiteBranding(ctx 
*gin.Context) {
        handler.HandleResponse(ctx, err, resp)
 }
 
-// GetSiteWrite get site interface
-// @Summary get site interface
-// @Description get site interface
+// GetSiteTag get site tags setting
+// @Summary get site tags setting
+// @Description get site tags setting
+// @Security ApiKeyAuth
+// @Tags admin
+// @Produce json
+// @Success 200 {object} handler.RespBody{data=schema.SiteTagsResp}
+// @Router /answer/admin/api/siteinfo/tag [get]
+func (sc *SiteInfoController) GetSiteTag(ctx *gin.Context) {
+       resp, err := sc.siteInfoService.GetSiteTag(ctx)
+       handler.HandleResponse(ctx, err, resp)
+}
+
+// GetSiteQuestion get site questions setting
+// @Summary get site questions setting
+// @Description get site questions setting
+// @Security ApiKeyAuth
+// @Tags admin
+// @Produce json
+// @Success 200 {object} handler.RespBody{data=schema.SiteQuestionsResp}
+// @Router /answer/admin/api/siteinfo/question [get]
+func (sc *SiteInfoController) GetSiteQuestion(ctx *gin.Context) {
+       resp, err := sc.siteInfoService.GetSiteQuestion(ctx)
+       handler.HandleResponse(ctx, err, resp)
+}
+
+// GetSiteAdvanced get site advanced setting
+// @Summary get site advanced setting
+// @Description get site advanced setting
 // @Security ApiKeyAuth
 // @Tags admin
 // @Produce json
-// @Success 200 {object} handler.RespBody{data=schema.SiteWriteResp}
-// @Router /answer/admin/api/siteinfo/write [get]
-func (sc *SiteInfoController) GetSiteWrite(ctx *gin.Context) {
-       resp, err := sc.siteInfoService.GetSiteWrite(ctx)
+// @Success 200 {object} handler.RespBody{data=schema.SiteAdvancedResp}
+// @Router /answer/admin/api/siteinfo/advanced [get]
+func (sc *SiteInfoController) GetSiteAdvanced(ctx *gin.Context) {
+       resp, err := sc.siteInfoService.GetSiteAdvanced(ctx)
        handler.HandleResponse(ctx, err, resp)
 }
 
@@ -288,23 +314,61 @@ func (sc *SiteInfoController) UpdateBranding(ctx 
*gin.Context) {
        handler.HandleResponse(ctx, saveErr, nil)
 }
 
-// UpdateSiteWrite update site write info
-// @Summary update site write info
-// @Description update site write info
+// UpdateSiteQuestion update site question settings
+// @Summary update site question settings
+// @Description update site question settings
+// @Security ApiKeyAuth
+// @Tags admin
+// @Produce json
+// @Param data body schema.SiteQuestionsReq true "questions settings"
+// @Success 200 {object} handler.RespBody{}
+// @Router /answer/admin/api/siteinfo/question [put]
+func (sc *SiteInfoController) UpdateSiteQuestion(ctx *gin.Context) {
+       req := &schema.SiteQuestionsReq{}
+       if handler.BindAndCheck(ctx, req) {
+               return
+       }
+
+       resp, err := sc.siteInfoService.SaveSiteQuestions(ctx, req)
+       handler.HandleResponse(ctx, err, resp)
+}
+
+// UpdateSiteTag update site tag settings
+// @Summary update site tag settings
+// @Description update site tag settings
 // @Security ApiKeyAuth
 // @Tags admin
 // @Produce json
-// @Param data body schema.SiteWriteReq true "write info"
+// @Param data body schema.SiteTagsReq true "tags settings"
 // @Success 200 {object} handler.RespBody{}
-// @Router /answer/admin/api/siteinfo/write [put]
-func (sc *SiteInfoController) UpdateSiteWrite(ctx *gin.Context) {
-       req := &schema.SiteWriteReq{}
+// @Router /answer/admin/api/siteinfo/tag [put]
+func (sc *SiteInfoController) UpdateSiteTag(ctx *gin.Context) {
+       req := &schema.SiteTagsReq{}
        if handler.BindAndCheck(ctx, req) {
                return
        }
        req.UserID = middleware.GetLoginUserIDFromContext(ctx)
 
-       resp, err := sc.siteInfoService.SaveSiteWrite(ctx, req)
+       resp, err := sc.siteInfoService.SaveSiteTags(ctx, req)
+       handler.HandleResponse(ctx, err, resp)
+}
+
+// UpdateSiteAdvanced update site advanced info
+// @Summary update site advanced info
+// @Description update site advanced info
+// @Security ApiKeyAuth
+// @Tags admin
+// @Produce json
+// @Param data body schema.SiteAdvancedReq true "advanced settings"
+// @Success 200 {object} handler.RespBody{}
+// @Router /answer/admin/api/siteinfo/advanced [put]
+func (sc *SiteInfoController) UpdateSiteAdvanced(ctx *gin.Context) {
+       req := &schema.SiteAdvancedReq{}
+       if handler.BindAndCheck(ctx, req) {
+               return
+       }
+
+       resp, err := sc.siteInfoService.SaveSiteAdvanced(ctx, req)
        handler.HandleResponse(ctx, err, resp)
 }
 
diff --git a/internal/migrations/migrations.go 
b/internal/migrations/migrations.go
index 9fda8c34..783332df 100644
--- a/internal/migrations/migrations.go
+++ b/internal/migrations/migrations.go
@@ -105,6 +105,7 @@ var migrations = []Migration{
        NewMigration("v1.6.0", "move user config to interface", 
moveUserConfigToInterface, true),
        NewMigration("v1.7.0", "add optional tags", addOptionalTags, true),
        NewMigration("v1.7.2", "expand avatar column length", 
expandAvatarColumnLength, false),
+       NewMigration("v1.8.0", "change admin menu", updateAdminMenuSettings, 
true),
 }
 
 func GetMigrations() []Migration {
diff --git a/internal/migrations/v30.go b/internal/migrations/v30.go
new file mode 100644
index 00000000..5d5d5223
--- /dev/null
+++ b/internal/migrations/v30.go
@@ -0,0 +1,173 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package migrations
+
+import (
+       "context"
+       "encoding/json"
+
+       "github.com/apache/answer/internal/base/constant"
+       "github.com/apache/answer/internal/base/reason"
+       "github.com/apache/answer/internal/entity"
+       "github.com/apache/answer/internal/schema"
+       "github.com/segmentfault/pacman/errors"
+       "xorm.io/builder"
+       "xorm.io/xorm"
+)
+
+func updateAdminMenuSettings(ctx context.Context, x *xorm.Engine) (err error) {
+       err = splitWriteMenu(ctx, x)
+       if err != nil {
+               return
+       }
+       return
+}
+
+// splitWriteMenu splits the site write settings into advanced, questions, and 
tags settings
+func splitWriteMenu(ctx context.Context, x *xorm.Engine) error {
+       var (
+               siteInfo          = &entity.SiteInfo{}
+               siteInfoAdvanced  = &entity.SiteInfo{}
+               siteInfoQuestions = &entity.SiteInfo{}
+               siteInfoTags      = &entity.SiteInfo{}
+       )
+       exist, err := x.Context(ctx).Where(builder.Eq{"type": 
constant.SiteTypeWrite}).Get(siteInfo)
+       if err != nil {
+               err = 
errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
+               return err
+       }
+       if !exist {
+               return nil
+       }
+       siteWrite := &schema.SiteWriteResp{}
+       if err := json.Unmarshal([]byte(siteInfo.Content), siteWrite); err != 
nil {
+               return err
+       }
+       // site advanced settings
+       siteAdvanced := &schema.SiteAdvancedResp{
+               MaxImageSize:                   siteWrite.MaxImageSize,
+               MaxAttachmentSize:              siteWrite.MaxAttachmentSize,
+               MaxImageMegapixel:              siteWrite.MaxImageMegapixel,
+               AuthorizedImageExtensions:      
siteWrite.AuthorizedImageExtensions,
+               AuthorizedAttachmentExtensions: 
siteWrite.AuthorizedAttachmentExtensions,
+       }
+       // site questions settings
+       siteQuestions := &schema.SiteQuestionsResp{
+               MinimumContent: siteWrite.MinimumContent,
+               RestrictAnswer: siteWrite.RestrictAnswer,
+       }
+       // site tags settings
+       siteTags := &schema.SiteTagsResp{
+               ReservedTags:  siteWrite.ReservedTags,
+               RecommendTags: siteWrite.RecommendTags,
+               MinimumTags:   siteWrite.MinimumTags,
+               RequiredTag:   siteWrite.RequiredTag,
+       }
+
+       // save site settings
+       // save advanced settings
+       existsAdvanced, err := x.Context(ctx).Where(builder.Eq{"type": 
constant.SiteTypeWrite}).Get(siteInfoAdvanced)
+       if err != nil {
+               return err
+       }
+       advancedContent, err := json.Marshal(siteAdvanced)
+       if err != nil {
+               return err
+       }
+       if existsAdvanced {
+               _, err = 
x.Context(ctx).ID(siteInfoAdvanced.ID).Update(&entity.SiteInfo{
+                       Type:    constant.SiteTypeAdvanced,
+                       Content: string(advancedContent),
+                       Status:  1,
+               })
+               if err != nil {
+                       return err
+               }
+       } else {
+               _, err = x.Context(ctx).Insert(&entity.SiteInfo{
+                       Type:    constant.SiteTypeAdvanced,
+                       Content: string(advancedContent),
+                       Status:  1,
+               })
+               if err != nil {
+                       return err
+               }
+       }
+
+       // save questions settings
+       existsQuestions, err := x.Context(ctx).Where(builder.Eq{"type": 
constant.SiteTypeQuestions}).Get(siteInfoQuestions)
+       if err != nil {
+               return err
+       }
+       questionsContent, err := json.Marshal(siteQuestions)
+       if err != nil {
+               return err
+       }
+       if existsQuestions {
+               _, err = 
x.Context(ctx).ID(siteInfoQuestions.ID).Update(&entity.SiteInfo{
+                       Type:    constant.SiteTypeQuestions,
+                       Content: string(questionsContent),
+                       Status:  1,
+               })
+               if err != nil {
+                       return err
+               }
+       } else {
+               _, err = x.Context(ctx).Insert(&entity.SiteInfo{
+                       Type:    constant.SiteTypeQuestions,
+                       Content: string(questionsContent),
+                       Status:  1,
+               })
+               if err != nil {
+                       return err
+               }
+       }
+
+       // save tags settings
+       existsTags, err := x.Context(ctx).Where(builder.Eq{"type": 
constant.SiteTypeTags}).Get(siteInfoTags)
+       if err != nil {
+               return err
+       }
+       tagsContent, err := json.Marshal(siteTags)
+       if err != nil {
+               return err
+       }
+       if existsTags {
+               _, err = 
x.Context(ctx).ID(siteInfoTags.ID).Update(&entity.SiteInfo{
+                       Type:    constant.SiteTypeTags,
+                       Content: string(tagsContent),
+                       Status:  1,
+               })
+               if err != nil {
+                       return err
+               }
+       } else {
+               _, err = x.Context(ctx).Insert(&entity.SiteInfo{
+                       Type:    constant.SiteTypeTags,
+                       Content: string(tagsContent),
+                       Status:  1,
+               })
+               if err != nil {
+                       return err
+               }
+       }
+
+       return nil
+}
diff --git a/internal/router/answer_api_router.go 
b/internal/router/answer_api_router.go
index 1191492b..d717bc9d 100644
--- a/internal/router/answer_api_router.go
+++ b/internal/router/answer_api_router.go
@@ -347,8 +347,14 @@ func (a *AnswerAPIRouter) RegisterAnswerAdminAPIRouter(r 
*gin.RouterGroup) {
        r.PUT("/siteinfo/interface", a.adminSiteInfoController.UpdateInterface)
        r.GET("/siteinfo/branding", a.adminSiteInfoController.GetSiteBranding)
        r.PUT("/siteinfo/branding", a.adminSiteInfoController.UpdateBranding)
-       r.GET("/siteinfo/write", a.adminSiteInfoController.GetSiteWrite)
-       r.PUT("/siteinfo/write", a.adminSiteInfoController.UpdateSiteWrite)
+
+       r.GET("/siteinfo/question", a.adminSiteInfoController.GetSiteQuestion)
+       r.PUT("/siteinfo/question", 
a.adminSiteInfoController.UpdateSiteQuestion)
+       r.GET("/siteinfo/tag", a.adminSiteInfoController.GetSiteTag)
+       r.PUT("/siteinfo/tag", a.adminSiteInfoController.UpdateSiteTag)
+       r.GET("/siteinfo/advanced", a.adminSiteInfoController.GetSiteAdvanced)
+       r.PUT("/siteinfo/advanced", 
a.adminSiteInfoController.UpdateSiteAdvanced)
+
        r.GET("/siteinfo/legal", a.adminSiteInfoController.GetSiteLegal)
        r.PUT("/siteinfo/legal", a.adminSiteInfoController.UpdateSiteLegal)
        r.GET("/siteinfo/seo", a.adminSiteInfoController.GetSeo)
diff --git a/internal/schema/siteinfo_schema.go 
b/internal/schema/siteinfo_schema.go
index 7ab65751..daf49146 100644
--- a/internal/schema/siteinfo_schema.go
+++ b/internal/schema/siteinfo_schema.go
@@ -89,21 +89,47 @@ type SiteWriteReq struct {
        UserID                         string          `json:"-"`
 }
 
-func (s *SiteWriteResp) GetMaxImageSize() int64 {
+type SiteWriteResp SiteWriteReq
+
+// SiteQuestionsReq site questions settings request
+type SiteQuestionsReq struct {
+       MinimumContent int  `validate:"omitempty,gte=0,lte=65535" 
json:"min_content"`
+       RestrictAnswer bool `validate:"omitempty" json:"restrict_answer"`
+}
+
+// SiteAdvancedReq site advanced settings request
+type SiteAdvancedReq struct {
+       MaxImageSize                   int      `validate:"omitempty,gt=0" 
json:"max_image_size"`
+       MaxAttachmentSize              int      `validate:"omitempty,gt=0" 
json:"max_attachment_size"`
+       MaxImageMegapixel              int      `validate:"omitempty,gt=0" 
json:"max_image_megapixel"`
+       AuthorizedImageExtensions      []string `validate:"omitempty" 
json:"authorized_image_extensions"`
+       AuthorizedAttachmentExtensions []string `validate:"omitempty" 
json:"authorized_attachment_extensions"`
+}
+
+// SiteTagsReq site tags settings request
+type SiteTagsReq struct {
+       ReservedTags  []*SiteWriteTag `validate:"omitempty,dive" 
json:"reserved_tags"`
+       RecommendTags []*SiteWriteTag `validate:"omitempty,dive" 
json:"recommend_tags"`
+       MinimumTags   int             `validate:"omitempty,gte=0,lte=5" 
json:"min_tags"`
+       RequiredTag   bool            `validate:"omitempty" json:"required_tag"`
+       UserID        string          `json:"-"`
+}
+
+func (s *SiteAdvancedResp) GetMaxImageSize() int64 {
        if s.MaxImageSize <= 0 {
                return constant.DefaultMaxImageSize
        }
        return int64(s.MaxImageSize) * 1024 * 1024
 }
 
-func (s *SiteWriteResp) GetMaxAttachmentSize() int64 {
+func (s *SiteAdvancedResp) GetMaxAttachmentSize() int64 {
        if s.MaxAttachmentSize <= 0 {
                return constant.DefaultMaxAttachmentSize
        }
        return int64(s.MaxAttachmentSize) * 1024 * 1024
 }
 
-func (s *SiteWriteResp) GetMaxImageMegapixel() int {
+func (s *SiteAdvancedResp) GetMaxImageMegapixel() int {
        if s.MaxImageMegapixel <= 0 {
                return constant.DefaultMaxImageMegapixel
        }
@@ -236,8 +262,9 @@ type ThemeOption struct {
        Value string `json:"value"`
 }
 
-// SiteWriteResp site write response
-type SiteWriteResp SiteWriteReq
+type SiteQuestionsResp SiteQuestionsReq
+type SiteAdvancedResp SiteAdvancedReq
+type SiteTagsResp SiteTagsReq
 
 // SiteLegalResp site write response
 type SiteLegalResp SiteLegalReq
@@ -260,7 +287,9 @@ type SiteInfoResp struct {
        CustomCssHtml *SiteCustomCssHTMLResp `json:"custom_css_html"`
        SiteSeo       *SiteSeoResp           `json:"site_seo"`
        SiteUsers     *SiteUsersResp         `json:"site_users"`
-       Write         *SiteWriteResp         `json:"site_write"`
+       Advanced      *SiteAdvancedResp      `json:"site_advanced"`
+       Questions     *SiteQuestionsResp     `json:"site_questions"`
+       Tags          *SiteTagsResp          `json:"site_tags"`
        Legal         *SiteLegalSimpleResp   `json:"site_legal"`
        Version       string                 `json:"version"`
        Revision      string                 `json:"revision"`
diff --git a/internal/service/mock/siteinfo_repo_mock.go 
b/internal/service/mock/siteinfo_repo_mock.go
index a98ceb68..5d5429ba 100644
--- a/internal/service/mock/siteinfo_repo_mock.go
+++ b/internal/service/mock/siteinfo_repo_mock.go
@@ -41,7 +41,6 @@ import (
 type MockSiteInfoRepo struct {
        ctrl     *gomock.Controller
        recorder *MockSiteInfoRepoMockRecorder
-       isgomock struct{}
 }
 
 // MockSiteInfoRepoMockRecorder is the mock recorder for MockSiteInfoRepo.
@@ -72,7 +71,7 @@ func (m *MockSiteInfoRepo) GetByType(ctx context.Context, 
siteType string) (*ent
 }
 
 // GetByType indicates an expected call of GetByType.
-func (mr *MockSiteInfoRepoMockRecorder) GetByType(ctx, siteType any) 
*gomock.Call {
+func (mr *MockSiteInfoRepoMockRecorder) GetByType(ctx, siteType interface{}) 
*gomock.Call {
        mr.mock.ctrl.T.Helper()
        return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetByType", 
reflect.TypeOf((*MockSiteInfoRepo)(nil).GetByType), ctx, siteType)
 }
@@ -87,7 +86,7 @@ func (m *MockSiteInfoRepo) IsBrandingFileUsed(ctx 
context.Context, filePath stri
 }
 
 // IsBrandingFileUsed indicates an expected call of IsBrandingFileUsed.
-func (mr *MockSiteInfoRepoMockRecorder) IsBrandingFileUsed(ctx, filePath any) 
*gomock.Call {
+func (mr *MockSiteInfoRepoMockRecorder) IsBrandingFileUsed(ctx, filePath 
interface{}) *gomock.Call {
        mr.mock.ctrl.T.Helper()
        return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, 
"IsBrandingFileUsed", 
reflect.TypeOf((*MockSiteInfoRepo)(nil).IsBrandingFileUsed), ctx, filePath)
 }
@@ -101,7 +100,7 @@ func (m *MockSiteInfoRepo) SaveByType(ctx context.Context, 
siteType string, data
 }
 
 // SaveByType indicates an expected call of SaveByType.
-func (mr *MockSiteInfoRepoMockRecorder) SaveByType(ctx, siteType, data any) 
*gomock.Call {
+func (mr *MockSiteInfoRepoMockRecorder) SaveByType(ctx, siteType, data 
interface{}) *gomock.Call {
        mr.mock.ctrl.T.Helper()
        return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveByType", 
reflect.TypeOf((*MockSiteInfoRepo)(nil).SaveByType), ctx, siteType, data)
 }
@@ -110,7 +109,6 @@ func (mr *MockSiteInfoRepoMockRecorder) SaveByType(ctx, 
siteType, data any) *gom
 type MockSiteInfoCommonService struct {
        ctrl     *gomock.Controller
        recorder *MockSiteInfoCommonServiceMockRecorder
-       isgomock struct{}
 }
 
 // MockSiteInfoCommonServiceMockRecorder is the mock recorder for 
MockSiteInfoCommonService.
@@ -139,7 +137,7 @@ func (m *MockSiteInfoCommonService) FormatAvatar(ctx 
context.Context, originalAv
 }
 
 // FormatAvatar indicates an expected call of FormatAvatar.
-func (mr *MockSiteInfoCommonServiceMockRecorder) FormatAvatar(ctx, 
originalAvatarData, email, userStatus any) *gomock.Call {
+func (mr *MockSiteInfoCommonServiceMockRecorder) FormatAvatar(ctx, 
originalAvatarData, email, userStatus interface{}) *gomock.Call {
        mr.mock.ctrl.T.Helper()
        return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FormatAvatar", 
reflect.TypeOf((*MockSiteInfoCommonService)(nil).FormatAvatar), ctx, 
originalAvatarData, email, userStatus)
 }
@@ -153,11 +151,26 @@ func (m *MockSiteInfoCommonService) FormatListAvatar(ctx 
context.Context, userLi
 }
 
 // FormatListAvatar indicates an expected call of FormatListAvatar.
-func (mr *MockSiteInfoCommonServiceMockRecorder) FormatListAvatar(ctx, 
userList any) *gomock.Call {
+func (mr *MockSiteInfoCommonServiceMockRecorder) FormatListAvatar(ctx, 
userList interface{}) *gomock.Call {
        mr.mock.ctrl.T.Helper()
        return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, 
"FormatListAvatar", 
reflect.TypeOf((*MockSiteInfoCommonService)(nil).FormatListAvatar), ctx, 
userList)
 }
 
+// GetSiteAdvanced mocks base method.
+func (m *MockSiteInfoCommonService) GetSiteAdvanced(ctx context.Context) 
(*schema.SiteAdvancedResp, error) {
+       m.ctrl.T.Helper()
+       ret := m.ctrl.Call(m, "GetSiteAdvanced", ctx)
+       ret0, _ := ret[0].(*schema.SiteAdvancedResp)
+       ret1, _ := ret[1].(error)
+       return ret0, ret1
+}
+
+// GetSiteAdvanced indicates an expected call of GetSiteAdvanced.
+func (mr *MockSiteInfoCommonServiceMockRecorder) GetSiteAdvanced(ctx 
interface{}) *gomock.Call {
+       mr.mock.ctrl.T.Helper()
+       return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, 
"GetSiteAdvanced", 
reflect.TypeOf((*MockSiteInfoCommonService)(nil).GetSiteAdvanced), ctx)
+}
+
 // GetSiteBranding mocks base method.
 func (m *MockSiteInfoCommonService) GetSiteBranding(ctx context.Context) 
(*schema.SiteBrandingResp, error) {
        m.ctrl.T.Helper()
@@ -168,7 +181,7 @@ func (m *MockSiteInfoCommonService) GetSiteBranding(ctx 
context.Context) (*schem
 }
 
 // GetSiteBranding indicates an expected call of GetSiteBranding.
-func (mr *MockSiteInfoCommonServiceMockRecorder) GetSiteBranding(ctx any) 
*gomock.Call {
+func (mr *MockSiteInfoCommonServiceMockRecorder) GetSiteBranding(ctx 
interface{}) *gomock.Call {
        mr.mock.ctrl.T.Helper()
        return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, 
"GetSiteBranding", 
reflect.TypeOf((*MockSiteInfoCommonService)(nil).GetSiteBranding), ctx)
 }
@@ -183,7 +196,7 @@ func (m *MockSiteInfoCommonService) 
GetSiteCustomCssHTML(ctx context.Context) (*
 }
 
 // GetSiteCustomCssHTML indicates an expected call of GetSiteCustomCssHTML.
-func (mr *MockSiteInfoCommonServiceMockRecorder) GetSiteCustomCssHTML(ctx any) 
*gomock.Call {
+func (mr *MockSiteInfoCommonServiceMockRecorder) GetSiteCustomCssHTML(ctx 
interface{}) *gomock.Call {
        mr.mock.ctrl.T.Helper()
        return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, 
"GetSiteCustomCssHTML", 
reflect.TypeOf((*MockSiteInfoCommonService)(nil).GetSiteCustomCssHTML), ctx)
 }
@@ -198,7 +211,7 @@ func (m *MockSiteInfoCommonService) GetSiteGeneral(ctx 
context.Context) (*schema
 }
 
 // GetSiteGeneral indicates an expected call of GetSiteGeneral.
-func (mr *MockSiteInfoCommonServiceMockRecorder) GetSiteGeneral(ctx any) 
*gomock.Call {
+func (mr *MockSiteInfoCommonServiceMockRecorder) GetSiteGeneral(ctx 
interface{}) *gomock.Call {
        mr.mock.ctrl.T.Helper()
        return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSiteGeneral", 
reflect.TypeOf((*MockSiteInfoCommonService)(nil).GetSiteGeneral), ctx)
 }
@@ -212,7 +225,7 @@ func (m *MockSiteInfoCommonService) GetSiteInfoByType(ctx 
context.Context, siteT
 }
 
 // GetSiteInfoByType indicates an expected call of GetSiteInfoByType.
-func (mr *MockSiteInfoCommonServiceMockRecorder) GetSiteInfoByType(ctx, 
siteType, resp any) *gomock.Call {
+func (mr *MockSiteInfoCommonServiceMockRecorder) GetSiteInfoByType(ctx, 
siteType, resp interface{}) *gomock.Call {
        mr.mock.ctrl.T.Helper()
        return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, 
"GetSiteInfoByType", 
reflect.TypeOf((*MockSiteInfoCommonService)(nil).GetSiteInfoByType), ctx, 
siteType, resp)
 }
@@ -227,7 +240,7 @@ func (m *MockSiteInfoCommonService) GetSiteInterface(ctx 
context.Context) (*sche
 }
 
 // GetSiteInterface indicates an expected call of GetSiteInterface.
-func (mr *MockSiteInfoCommonServiceMockRecorder) GetSiteInterface(ctx any) 
*gomock.Call {
+func (mr *MockSiteInfoCommonServiceMockRecorder) GetSiteInterface(ctx 
interface{}) *gomock.Call {
        mr.mock.ctrl.T.Helper()
        return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, 
"GetSiteInterface", 
reflect.TypeOf((*MockSiteInfoCommonService)(nil).GetSiteInterface), ctx)
 }
@@ -242,7 +255,7 @@ func (m *MockSiteInfoCommonService) GetSiteLegal(ctx 
context.Context) (*schema.S
 }
 
 // GetSiteLegal indicates an expected call of GetSiteLegal.
-func (mr *MockSiteInfoCommonServiceMockRecorder) GetSiteLegal(ctx any) 
*gomock.Call {
+func (mr *MockSiteInfoCommonServiceMockRecorder) GetSiteLegal(ctx interface{}) 
*gomock.Call {
        mr.mock.ctrl.T.Helper()
        return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSiteLegal", 
reflect.TypeOf((*MockSiteInfoCommonService)(nil).GetSiteLegal), ctx)
 }
@@ -257,11 +270,26 @@ func (m *MockSiteInfoCommonService) GetSiteLogin(ctx 
context.Context) (*schema.S
 }
 
 // GetSiteLogin indicates an expected call of GetSiteLogin.
-func (mr *MockSiteInfoCommonServiceMockRecorder) GetSiteLogin(ctx any) 
*gomock.Call {
+func (mr *MockSiteInfoCommonServiceMockRecorder) GetSiteLogin(ctx interface{}) 
*gomock.Call {
        mr.mock.ctrl.T.Helper()
        return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSiteLogin", 
reflect.TypeOf((*MockSiteInfoCommonService)(nil).GetSiteLogin), ctx)
 }
 
+// GetSiteQuestion mocks base method.
+func (m *MockSiteInfoCommonService) GetSiteQuestion(ctx context.Context) 
(*schema.SiteQuestionsResp, error) {
+       m.ctrl.T.Helper()
+       ret := m.ctrl.Call(m, "GetSiteQuestion", ctx)
+       ret0, _ := ret[0].(*schema.SiteQuestionsResp)
+       ret1, _ := ret[1].(error)
+       return ret0, ret1
+}
+
+// GetSiteQuestion indicates an expected call of GetSiteQuestion.
+func (mr *MockSiteInfoCommonServiceMockRecorder) GetSiteQuestion(ctx 
interface{}) *gomock.Call {
+       mr.mock.ctrl.T.Helper()
+       return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, 
"GetSiteQuestion", 
reflect.TypeOf((*MockSiteInfoCommonService)(nil).GetSiteQuestion), ctx)
+}
+
 // GetSiteSeo mocks base method.
 func (m *MockSiteInfoCommonService) GetSiteSeo(ctx context.Context) 
(*schema.SiteSeoResp, error) {
        m.ctrl.T.Helper()
@@ -272,11 +300,26 @@ func (m *MockSiteInfoCommonService) GetSiteSeo(ctx 
context.Context) (*schema.Sit
 }
 
 // GetSiteSeo indicates an expected call of GetSiteSeo.
-func (mr *MockSiteInfoCommonServiceMockRecorder) GetSiteSeo(ctx any) 
*gomock.Call {
+func (mr *MockSiteInfoCommonServiceMockRecorder) GetSiteSeo(ctx interface{}) 
*gomock.Call {
        mr.mock.ctrl.T.Helper()
        return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSiteSeo", 
reflect.TypeOf((*MockSiteInfoCommonService)(nil).GetSiteSeo), ctx)
 }
 
+// GetSiteTag mocks base method.
+func (m *MockSiteInfoCommonService) GetSiteTag(ctx context.Context) 
(*schema.SiteTagsResp, error) {
+       m.ctrl.T.Helper()
+       ret := m.ctrl.Call(m, "GetSiteTag", ctx)
+       ret0, _ := ret[0].(*schema.SiteTagsResp)
+       ret1, _ := ret[1].(error)
+       return ret0, ret1
+}
+
+// GetSiteTag indicates an expected call of GetSiteTag.
+func (mr *MockSiteInfoCommonServiceMockRecorder) GetSiteTag(ctx interface{}) 
*gomock.Call {
+       mr.mock.ctrl.T.Helper()
+       return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSiteTag", 
reflect.TypeOf((*MockSiteInfoCommonService)(nil).GetSiteTag), ctx)
+}
+
 // GetSiteTheme mocks base method.
 func (m *MockSiteInfoCommonService) GetSiteTheme(ctx context.Context) 
(*schema.SiteThemeResp, error) {
        m.ctrl.T.Helper()
@@ -287,7 +330,7 @@ func (m *MockSiteInfoCommonService) GetSiteTheme(ctx 
context.Context) (*schema.S
 }
 
 // GetSiteTheme indicates an expected call of GetSiteTheme.
-func (mr *MockSiteInfoCommonServiceMockRecorder) GetSiteTheme(ctx any) 
*gomock.Call {
+func (mr *MockSiteInfoCommonServiceMockRecorder) GetSiteTheme(ctx interface{}) 
*gomock.Call {
        mr.mock.ctrl.T.Helper()
        return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSiteTheme", 
reflect.TypeOf((*MockSiteInfoCommonService)(nil).GetSiteTheme), ctx)
 }
@@ -302,7 +345,7 @@ func (m *MockSiteInfoCommonService) GetSiteUsers(ctx 
context.Context) (*schema.S
 }
 
 // GetSiteUsers indicates an expected call of GetSiteUsers.
-func (mr *MockSiteInfoCommonServiceMockRecorder) GetSiteUsers(ctx any) 
*gomock.Call {
+func (mr *MockSiteInfoCommonServiceMockRecorder) GetSiteUsers(ctx interface{}) 
*gomock.Call {
        mr.mock.ctrl.T.Helper()
        return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSiteUsers", 
reflect.TypeOf((*MockSiteInfoCommonService)(nil).GetSiteUsers), ctx)
 }
@@ -317,7 +360,7 @@ func (m *MockSiteInfoCommonService) GetSiteWrite(ctx 
context.Context) (*schema.S
 }
 
 // GetSiteWrite indicates an expected call of GetSiteWrite.
-func (mr *MockSiteInfoCommonServiceMockRecorder) GetSiteWrite(ctx any) 
*gomock.Call {
+func (mr *MockSiteInfoCommonServiceMockRecorder) GetSiteWrite(ctx interface{}) 
*gomock.Call {
        mr.mock.ctrl.T.Helper()
        return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSiteWrite", 
reflect.TypeOf((*MockSiteInfoCommonService)(nil).GetSiteWrite), ctx)
 }
@@ -331,7 +374,7 @@ func (m *MockSiteInfoCommonService) IsBrandingFileUsed(ctx 
context.Context, file
 }
 
 // IsBrandingFileUsed indicates an expected call of IsBrandingFileUsed.
-func (mr *MockSiteInfoCommonServiceMockRecorder) IsBrandingFileUsed(ctx, 
filePath any) *gomock.Call {
+func (mr *MockSiteInfoCommonServiceMockRecorder) IsBrandingFileUsed(ctx, 
filePath interface{}) *gomock.Call {
        mr.mock.ctrl.T.Helper()
        return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, 
"IsBrandingFileUsed", 
reflect.TypeOf((*MockSiteInfoCommonService)(nil).IsBrandingFileUsed), ctx, 
filePath)
 }
diff --git a/internal/service/question_common/question.go 
b/internal/service/question_common/question.go
index 557a5db1..3a730634 100644
--- a/internal/service/question_common/question.go
+++ b/internal/service/question_common/question.go
@@ -900,7 +900,7 @@ func (qs *QuestionCommon) tryToGetQuestionIDFromMsg(ctx 
context.Context, closeMs
 }
 
 func (qs *QuestionCommon) GetMinimumContentLength(ctx context.Context) (int, 
error) {
-       siteInfo, err := qs.siteInfoService.GetSiteWrite(ctx)
+       siteInfo, err := qs.siteInfoService.GetSiteQuestion(ctx)
        if err != nil {
                return 6, err
        }
diff --git a/internal/service/siteinfo/siteinfo_service.go 
b/internal/service/siteinfo/siteinfo_service.go
index f355d09f..b633ed42 100644
--- a/internal/service/siteinfo/siteinfo_service.go
+++ b/internal/service/siteinfo/siteinfo_service.go
@@ -103,17 +103,14 @@ func (s *SiteInfoService) GetSiteUsers(ctx 
context.Context) (resp *schema.SiteUs
        return s.siteInfoCommonService.GetSiteUsers(ctx)
 }
 
-// GetSiteWrite get site info write
-func (s *SiteInfoService) GetSiteWrite(ctx context.Context) (resp 
*schema.SiteWriteResp, err error) {
-       resp = &schema.SiteWriteResp{}
-       siteInfo, exist, err := s.siteInfoRepo.GetByType(ctx, 
constant.SiteTypeWrite)
+// GetSiteTag get site info write
+func (s *SiteInfoService) GetSiteTag(ctx context.Context) (resp 
*schema.SiteTagsResp, err error) {
+       resp = &schema.SiteTagsResp{}
+       _, err = s.siteInfoCommonService.GetSiteTag(ctx)
        if err != nil {
                log.Error(err)
                return resp, nil
        }
-       if exist {
-               _ = json.Unmarshal([]byte(siteInfo.Content), resp)
-       }
 
        resp.RecommendTags, err = 
s.tagCommonService.GetSiteWriteRecommendTag(ctx)
        if err != nil {
@@ -126,6 +123,16 @@ func (s *SiteInfoService) GetSiteWrite(ctx 
context.Context) (resp *schema.SiteWr
        return resp, nil
 }
 
+// GetSiteQuestion get site questions settings
+func (s *SiteInfoService) GetSiteQuestion(ctx context.Context) (resp 
*schema.SiteQuestionsResp, err error) {
+       return s.siteInfoCommonService.GetSiteQuestion(ctx)
+}
+
+// GetSiteAdvanced get site advanced settings
+func (s *SiteInfoService) GetSiteAdvanced(ctx context.Context) (resp 
*schema.SiteAdvancedResp, err error) {
+       return s.siteInfoCommonService.GetSiteAdvanced(ctx)
+}
+
 // GetSiteLegal get site legal info
 func (s *SiteInfoService) GetSiteLegal(ctx context.Context) (resp 
*schema.SiteLegalResp, err error) {
        return s.siteInfoCommonService.GetSiteLegal(ctx)
@@ -182,8 +189,30 @@ func (s *SiteInfoService) SaveSiteBranding(ctx 
context.Context, req *schema.Site
        return s.siteInfoRepo.SaveByType(ctx, constant.SiteTypeBranding, data)
 }
 
-// SaveSiteWrite save site configuration about write
-func (s *SiteInfoService) SaveSiteWrite(ctx context.Context, req 
*schema.SiteWriteReq) (resp any, err error) {
+// SaveSiteAdvanced save site advanced configuration
+func (s *SiteInfoService) SaveSiteAdvanced(ctx context.Context, req 
*schema.SiteAdvancedReq) (resp any, err error) {
+       content, _ := json.Marshal(req)
+       data := &entity.SiteInfo{
+               Type:    constant.SiteTypeAdvanced,
+               Content: string(content),
+               Status:  1,
+       }
+       return nil, s.siteInfoRepo.SaveByType(ctx, constant.SiteTypeAdvanced, 
data)
+}
+
+// SaveSiteQuestions save site questions configuration
+func (s *SiteInfoService) SaveSiteQuestions(ctx context.Context, req 
*schema.SiteQuestionsReq) (resp any, err error) {
+       content, _ := json.Marshal(req)
+       data := &entity.SiteInfo{
+               Type:    constant.SiteTypeQuestions,
+               Content: string(content),
+               Status:  1,
+       }
+       return nil, s.siteInfoRepo.SaveByType(ctx, constant.SiteTypeQuestions, 
data)
+}
+
+// SaveSiteTags save site tags configuration
+func (s *SiteInfoService) SaveSiteTags(ctx context.Context, req 
*schema.SiteTagsReq) (resp any, err error) {
        recommendTags, reservedTags := make([]string, 0), make([]string, 0)
        recommendTagMapping, reservedTagMapping := make(map[string]bool), 
make(map[string]bool)
        for _, tag := range req.ReservedTags {
@@ -210,11 +239,11 @@ func (s *SiteInfoService) SaveSiteWrite(ctx 
context.Context, req *schema.SiteWri
 
        content, _ := json.Marshal(req)
        data := &entity.SiteInfo{
-               Type:    constant.SiteTypeWrite,
+               Type:    constant.SiteTypeTags,
                Content: string(content),
                Status:  1,
        }
-       return nil, s.siteInfoRepo.SaveByType(ctx, constant.SiteTypeWrite, data)
+       return nil, s.siteInfoRepo.SaveByType(ctx, constant.SiteTypeTags, data)
 }
 
 // SaveSiteLegal save site legal configuration
diff --git a/internal/service/siteinfo_common/siteinfo_service.go 
b/internal/service/siteinfo_common/siteinfo_service.go
index fda11722..87bc7ee1 100644
--- a/internal/service/siteinfo_common/siteinfo_service.go
+++ b/internal/service/siteinfo_common/siteinfo_service.go
@@ -51,6 +51,9 @@ type SiteInfoCommonService interface {
        FormatAvatar(ctx context.Context, originalAvatarData, email string, 
userStatus int) *schema.AvatarInfo
        FormatListAvatar(ctx context.Context, userList []*entity.User) 
(userID2AvatarMapping map[string]*schema.AvatarInfo)
        GetSiteWrite(ctx context.Context) (resp *schema.SiteWriteResp, err 
error)
+       GetSiteAdvanced(ctx context.Context) (resp *schema.SiteAdvancedResp, 
err error)
+       GetSiteQuestion(ctx context.Context) (resp *schema.SiteQuestionsResp, 
err error)
+       GetSiteTag(ctx context.Context) (resp *schema.SiteTagsResp, err error)
        GetSiteLegal(ctx context.Context) (resp *schema.SiteLegalResp, err 
error)
        GetSiteLogin(ctx context.Context) (resp *schema.SiteLoginResp, err 
error)
        GetSiteCustomCssHTML(ctx context.Context) (resp 
*schema.SiteCustomCssHTMLResp, err error)
@@ -167,6 +170,33 @@ func (s *siteInfoCommonService) GetSiteWrite(ctx 
context.Context) (resp *schema.
        return resp, nil
 }
 
+// GetSiteAdvanced get site info advanced
+func (s *siteInfoCommonService) GetSiteAdvanced(ctx context.Context) (resp 
*schema.SiteAdvancedResp, err error) {
+       resp = &schema.SiteAdvancedResp{}
+       if err = s.GetSiteInfoByType(ctx, constant.SiteTypeAdvanced, resp); err 
!= nil {
+               return nil, err
+       }
+       return resp, nil
+}
+
+// GetSiteQuestion get site info question
+func (s *siteInfoCommonService) GetSiteQuestion(ctx context.Context) (resp 
*schema.SiteQuestionsResp, err error) {
+       resp = &schema.SiteQuestionsResp{}
+       if err = s.GetSiteInfoByType(ctx, constant.SiteTypeQuestions, resp); 
err != nil {
+               return nil, err
+       }
+       return resp, nil
+}
+
+// GetSiteTag get site info tag
+func (s *siteInfoCommonService) GetSiteTag(ctx context.Context) (resp 
*schema.SiteTagsResp, err error) {
+       resp = &schema.SiteTagsResp{}
+       if err = s.GetSiteInfoByType(ctx, constant.SiteTypeTags, resp); err != 
nil {
+               return nil, err
+       }
+       return resp, nil
+}
+
 // GetSiteLegal get site info write
 func (s *siteInfoCommonService) GetSiteLegal(ctx context.Context) (resp 
*schema.SiteLegalResp, err error) {
        resp = &schema.SiteLegalResp{}
diff --git a/internal/service/tag_common/tag_common.go 
b/internal/service/tag_common/tag_common.go
index 9ca8e100..0da9f6fd 100644
--- a/internal/service/tag_common/tag_common.go
+++ b/internal/service/tag_common/tag_common.go
@@ -270,7 +270,7 @@ func (ts *TagCommonService) GetTagListByNames(ctx 
context.Context, tagNames []st
 }
 
 func (ts *TagCommonService) ExistRecommend(ctx context.Context, tags 
[]*schema.TagItem) (bool, error) {
-       taginfo, err := ts.siteInfoService.GetSiteWrite(ctx)
+       taginfo, err := ts.siteInfoService.GetSiteTag(ctx)
        if err != nil {
                return false, err
        }
@@ -295,7 +295,7 @@ func (ts *TagCommonService) ExistRecommend(ctx 
context.Context, tags []*schema.T
 }
 
 func (ts *TagCommonService) GetMinimumTags(ctx context.Context) (int, error) {
-       siteInfo, err := ts.siteInfoService.GetSiteWrite(ctx)
+       siteInfo, err := ts.siteInfoService.GetSiteTag(ctx)
        if err != nil {
                return 1, err
        }
@@ -469,7 +469,7 @@ func (ts *TagCommonService) 
TagsFormatRecommendAndReserved(ctx context.Context,
        if len(tagList) == 0 {
                return
        }
-       tagConfig, err := ts.siteInfoService.GetSiteWrite(ctx)
+       tagConfig, err := ts.siteInfoService.GetSiteTag(ctx)
        if err != nil {
                log.Error(err)
                return
@@ -485,7 +485,7 @@ func (ts *TagCommonService) 
tagFormatRecommendAndReserved(ctx context.Context, t
        if tag == nil {
                return
        }
-       tagConfig, err := ts.siteInfoService.GetSiteWrite(ctx)
+       tagConfig, err := ts.siteInfoService.GetSiteTag(ctx)
        if err != nil {
                log.Error(err)
                return
diff --git a/internal/service/uploader/upload.go 
b/internal/service/uploader/upload.go
index 8dea746c..58f80846 100644
--- a/internal/service/uploader/upload.go
+++ b/internal/service/uploader/upload.go
@@ -109,12 +109,12 @@ func (us *uploaderService) UploadAvatarFile(ctx 
*gin.Context, userID string) (ur
                return url, nil
        }
 
-       siteWrite, err := us.siteInfoService.GetSiteWrite(ctx)
+       siteAdvanced, err := us.siteInfoService.GetSiteAdvanced(ctx)
        if err != nil {
                return "", err
        }
 
-       ctx.Request.Body = http.MaxBytesReader(ctx.Writer, ctx.Request.Body, 
siteWrite.GetMaxImageSize())
+       ctx.Request.Body = http.MaxBytesReader(ctx.Writer, ctx.Request.Body, 
siteAdvanced.GetMaxImageSize())
        file, fileHeader, err := ctx.Request.FormFile("file")
        if err != nil {
                return "", 
errors.BadRequest(reason.RequestFormatError).WithError(err)
@@ -201,12 +201,12 @@ func (us *uploaderService) UploadPostFile(ctx 
*gin.Context, userID string) (
                return url, nil
        }
 
-       siteWrite, err := us.siteInfoService.GetSiteWrite(ctx)
+       siteAdvanced, err := us.siteInfoService.GetSiteAdvanced(ctx)
        if err != nil {
                return "", err
        }
 
-       ctx.Request.Body = http.MaxBytesReader(ctx.Writer, ctx.Request.Body, 
siteWrite.GetMaxImageSize())
+       ctx.Request.Body = http.MaxBytesReader(ctx.Writer, ctx.Request.Body, 
siteAdvanced.GetMaxImageSize())
        file, fileHeader, err := ctx.Request.FormFile("file")
        if err != nil {
                return "", 
errors.BadRequest(reason.RequestFormatError).WithError(err)
@@ -214,7 +214,7 @@ func (us *uploaderService) UploadPostFile(ctx *gin.Context, 
userID string) (
        defer func() {
                _ = file.Close()
        }()
-       if checker.IsUnAuthorizedExtension(fileHeader.Filename, 
siteWrite.AuthorizedImageExtensions) {
+       if checker.IsUnAuthorizedExtension(fileHeader.Filename, 
siteAdvanced.AuthorizedImageExtensions) {
                return "", 
errors.BadRequest(reason.RequestFormatError).WithError(err)
        }
 
@@ -239,7 +239,7 @@ func (us *uploaderService) UploadPostAttachment(ctx 
*gin.Context, userID string)
                return url, nil
        }
 
-       resp, err := us.siteInfoService.GetSiteWrite(ctx)
+       resp, err := us.siteInfoService.GetSiteAdvanced(ctx)
        if err != nil {
                return "", err
        }
@@ -277,12 +277,12 @@ func (us *uploaderService) UploadBrandingFile(ctx 
*gin.Context, userID string) (
                return url, nil
        }
 
-       siteWrite, err := us.siteInfoService.GetSiteWrite(ctx)
+       siteAdvanced, err := us.siteInfoService.GetSiteAdvanced(ctx)
        if err != nil {
                return "", err
        }
 
-       ctx.Request.Body = http.MaxBytesReader(ctx.Writer, ctx.Request.Body, 
siteWrite.GetMaxImageSize())
+       ctx.Request.Body = http.MaxBytesReader(ctx.Writer, ctx.Request.Body, 
siteAdvanced.GetMaxImageSize())
        file, fileHeader, err := ctx.Request.FormFile("file")
        if err != nil {
                return "", 
errors.BadRequest(reason.RequestFormatError).WithError(err)
@@ -311,7 +311,7 @@ func (us *uploaderService) uploadImageFile(ctx 
*gin.Context, file *multipart.Fil
        if err != nil {
                return "", err
        }
-       siteWrite, err := us.siteInfoService.GetSiteWrite(ctx)
+       siteAdvanced, err := us.siteInfoService.GetSiteAdvanced(ctx)
        if err != nil {
                return "", err
        }
@@ -328,7 +328,7 @@ func (us *uploaderService) uploadImageFile(ctx 
*gin.Context, file *multipart.Fil
                _ = src.Close()
        }()
 
-       if !checker.DecodeAndCheckImageFile(filePath, 
siteWrite.GetMaxImageMegapixel()) {
+       if !checker.DecodeAndCheckImageFile(filePath, 
siteAdvanced.GetMaxImageMegapixel()) {
                return "", 
errors.BadRequest(reason.UploadFileUnsupportedFileFormat)
        }
 
@@ -364,17 +364,17 @@ func (us *uploaderService) uploadAttachmentFile(ctx 
*gin.Context, file *multipar
 
 func (us *uploaderService) tryToUploadByPlugin(ctx *gin.Context, source 
plugin.UploadSource) (
        url string, err error) {
-       siteWrite, err := us.siteInfoService.GetSiteWrite(ctx)
+       siteAdvanced, err := us.siteInfoService.GetSiteAdvanced(ctx)
        if err != nil {
                return "", err
        }
        cond := plugin.UploadFileCondition{
                Source:                         source,
-               MaxImageSize:                   siteWrite.MaxImageSize,
-               MaxAttachmentSize:              siteWrite.MaxAttachmentSize,
-               MaxImageMegapixel:              siteWrite.MaxImageMegapixel,
-               AuthorizedImageExtensions:      
siteWrite.AuthorizedImageExtensions,
-               AuthorizedAttachmentExtensions: 
siteWrite.AuthorizedAttachmentExtensions,
+               MaxImageSize:                   siteAdvanced.MaxImageSize,
+               MaxAttachmentSize:              siteAdvanced.MaxAttachmentSize,
+               MaxImageMegapixel:              siteAdvanced.MaxImageMegapixel,
+               AuthorizedImageExtensions:      
siteAdvanced.AuthorizedImageExtensions,
+               AuthorizedAttachmentExtensions: 
siteAdvanced.AuthorizedAttachmentExtensions,
        }
        _ = plugin.CallStorage(func(fn plugin.Storage) error {
                resp := fn.UploadFile(ctx, cond)

Reply via email to