| /* |
| * 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 controller |
| |
| import ( |
| "net/http" |
| |
| "github.com/apache/answer/internal/base/handler" |
| "github.com/apache/answer/internal/base/middleware" |
| "github.com/apache/answer/internal/base/pager" |
| "github.com/apache/answer/internal/base/reason" |
| "github.com/apache/answer/internal/base/translator" |
| "github.com/apache/answer/internal/base/validator" |
| "github.com/apache/answer/internal/entity" |
| "github.com/apache/answer/internal/schema" |
| "github.com/apache/answer/internal/service/action" |
| "github.com/apache/answer/internal/service/content" |
| "github.com/apache/answer/internal/service/permission" |
| "github.com/apache/answer/internal/service/rank" |
| "github.com/apache/answer/internal/service/siteinfo_common" |
| "github.com/apache/answer/pkg/uid" |
| "github.com/gin-gonic/gin" |
| "github.com/jinzhu/copier" |
| "github.com/segmentfault/pacman/errors" |
| ) |
| |
| // QuestionController question controller |
| type QuestionController struct { |
| questionService *content.QuestionService |
| answerService *content.AnswerService |
| rankService *rank.RankService |
| siteInfoService siteinfo_common.SiteInfoCommonService |
| actionService *action.CaptchaService |
| rateLimitMiddleware *middleware.RateLimitMiddleware |
| } |
| |
| // NewQuestionController new controller |
| func NewQuestionController( |
| questionService *content.QuestionService, |
| answerService *content.AnswerService, |
| rankService *rank.RankService, |
| siteInfoService siteinfo_common.SiteInfoCommonService, |
| actionService *action.CaptchaService, |
| rateLimitMiddleware *middleware.RateLimitMiddleware, |
| ) *QuestionController { |
| return &QuestionController{ |
| questionService: questionService, |
| answerService: answerService, |
| rankService: rankService, |
| siteInfoService: siteInfoService, |
| actionService: actionService, |
| rateLimitMiddleware: rateLimitMiddleware, |
| } |
| } |
| |
| // RemoveQuestion delete question |
| // @Summary delete question |
| // @Description delete question |
| // @Tags Question |
| // @Accept json |
| // @Produce json |
| // @Security ApiKeyAuth |
| // @Param data body schema.RemoveQuestionReq true "question" |
| // @Success 200 {object} handler.RespBody |
| // @Router /answer/api/v1/question [delete] |
| func (qc *QuestionController) RemoveQuestion(ctx *gin.Context) { |
| req := &schema.RemoveQuestionReq{} |
| if handler.BindAndCheck(ctx, req) { |
| return |
| } |
| req.ID = uid.DeShortID(req.ID) |
| req.UserID = middleware.GetLoginUserIDFromContext(ctx) |
| req.IsAdmin = middleware.GetIsAdminFromContext(ctx) |
| isAdmin := middleware.GetUserIsAdminModerator(ctx) |
| if !isAdmin { |
| captchaPass := qc.actionService.ActionRecordVerifyCaptcha(ctx, entity.CaptchaActionDelete, req.UserID, req.CaptchaID, req.CaptchaCode) |
| if !captchaPass { |
| errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{ |
| ErrorField: "captcha_code", |
| ErrorMsg: translator.Tr(handler.GetLang(ctx), reason.CaptchaVerificationFailed), |
| }) |
| handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields) |
| return |
| } |
| } |
| |
| can, err := qc.rankService.CheckOperationPermission(ctx, req.UserID, permission.QuestionDelete, req.ID) |
| if err != nil { |
| handler.HandleResponse(ctx, err, nil) |
| return |
| } |
| if !can { |
| handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil) |
| return |
| } |
| err = qc.questionService.RemoveQuestion(ctx, req) |
| if !isAdmin { |
| qc.actionService.ActionRecordAdd(ctx, entity.CaptchaActionDelete, req.UserID) |
| } |
| handler.HandleResponse(ctx, err, nil) |
| } |
| |
| // OperationQuestion Operation question |
| // @Summary Operation question |
| // @Description Operation question \n operation [pin unpin hide show] |
| // @Tags Question |
| // @Accept json |
| // @Produce json |
| // @Security ApiKeyAuth |
| // @Param data body schema.OperationQuestionReq true "question" |
| // @Success 200 {object} handler.RespBody |
| // @Router /answer/api/v1/question/operation [put] |
| func (qc *QuestionController) OperationQuestion(ctx *gin.Context) { |
| req := &schema.OperationQuestionReq{} |
| if handler.BindAndCheck(ctx, req) { |
| return |
| } |
| req.ID = uid.DeShortID(req.ID) |
| req.UserID = middleware.GetLoginUserIDFromContext(ctx) |
| canList, err := qc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{ |
| permission.QuestionPin, |
| permission.QuestionUnPin, |
| permission.QuestionHide, |
| permission.QuestionShow, |
| }) |
| if err != nil { |
| handler.HandleResponse(ctx, err, nil) |
| return |
| } |
| req.CanPin = canList[0] |
| req.CanList = canList[1] |
| if (req.Operation == schema.QuestionOperationPin || req.Operation == schema.QuestionOperationUnPin) && !req.CanPin { |
| handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil) |
| return |
| } |
| if (req.Operation == schema.QuestionOperationHide || req.Operation == schema.QuestionOperationShow) && !req.CanList { |
| handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil) |
| return |
| } |
| err = qc.questionService.OperationQuestion(ctx, req) |
| handler.HandleResponse(ctx, err, nil) |
| } |
| |
| // CloseQuestion Close question |
| // @Summary Close question |
| // @Description Close question |
| // @Tags Question |
| // @Accept json |
| // @Produce json |
| // @Security ApiKeyAuth |
| // @Param data body schema.CloseQuestionReq true "question" |
| // @Success 200 {object} handler.RespBody |
| // @Router /answer/api/v1/question/status [put] |
| func (qc *QuestionController) CloseQuestion(ctx *gin.Context) { |
| req := &schema.CloseQuestionReq{} |
| if handler.BindAndCheck(ctx, req) { |
| return |
| } |
| req.ID = uid.DeShortID(req.ID) |
| req.UserID = middleware.GetLoginUserIDFromContext(ctx) |
| can, err := qc.rankService.CheckOperationPermission(ctx, req.UserID, permission.QuestionClose, "") |
| if err != nil { |
| handler.HandleResponse(ctx, err, nil) |
| return |
| } |
| if !can { |
| handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil) |
| return |
| } |
| |
| err = qc.questionService.CloseQuestion(ctx, req) |
| handler.HandleResponse(ctx, err, nil) |
| } |
| |
| // ReopenQuestion reopen question |
| // @Summary reopen question |
| // @Description reopen question |
| // @Tags Question |
| // @Accept json |
| // @Produce json |
| // @Security ApiKeyAuth |
| // @Param data body schema.ReopenQuestionReq true "question" |
| // @Success 200 {object} handler.RespBody |
| // @Router /answer/api/v1/question/reopen [put] |
| func (qc *QuestionController) ReopenQuestion(ctx *gin.Context) { |
| req := &schema.ReopenQuestionReq{} |
| if handler.BindAndCheck(ctx, req) { |
| return |
| } |
| req.QuestionID = uid.DeShortID(req.QuestionID) |
| req.UserID = middleware.GetLoginUserIDFromContext(ctx) |
| can, err := qc.rankService.CheckOperationPermission(ctx, req.UserID, permission.QuestionReopen, "") |
| if err != nil { |
| handler.HandleResponse(ctx, err, nil) |
| return |
| } |
| if !can { |
| handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil) |
| return |
| } |
| |
| err = qc.questionService.ReopenQuestion(ctx, req) |
| handler.HandleResponse(ctx, err, nil) |
| } |
| |
| // GetQuestion get question details |
| // @Summary get question details |
| // @Description get question details |
| // @Tags Question |
| // @Accept json |
| // @Produce json |
| // @Param id query string true "Question TagID" default(1) |
| // @Success 200 {string} string "" |
| // @Router /answer/api/v1/question/info [get] |
| func (qc *QuestionController) GetQuestion(ctx *gin.Context) { |
| id := ctx.Query("id") |
| id = uid.DeShortID(id) |
| userID := middleware.GetLoginUserIDFromContext(ctx) |
| req := schema.QuestionPermission{} |
| canList, err := qc.rankService.CheckOperationPermissions(ctx, userID, []string{ |
| permission.QuestionEdit, |
| permission.QuestionDelete, |
| permission.QuestionClose, |
| permission.QuestionReopen, |
| permission.QuestionPin, |
| permission.QuestionUnPin, |
| permission.QuestionHide, |
| permission.QuestionShow, |
| permission.AnswerInviteSomeoneToAnswer, |
| permission.QuestionUnDelete, |
| }) |
| if err != nil { |
| handler.HandleResponse(ctx, err, nil) |
| return |
| } |
| objectOwner := qc.rankService.CheckOperationObjectOwner(ctx, userID, id) |
| |
| req.CanEdit = canList[0] || objectOwner |
| req.CanDelete = canList[1] |
| req.CanClose = canList[2] |
| req.CanReopen = canList[3] |
| req.CanPin = canList[4] |
| req.CanUnPin = canList[5] |
| req.CanHide = canList[6] |
| req.CanShow = canList[7] |
| req.CanInviteOtherToAnswer = canList[8] |
| req.CanRecover = canList[9] |
| |
| info, err := qc.questionService.GetQuestionAndAddPV(ctx, id, userID, req) |
| if err != nil { |
| handler.HandleResponse(ctx, err, nil) |
| return |
| } |
| if handler.GetEnableShortID(ctx) { |
| info.ID = uid.EnShortID(info.ID) |
| } |
| handler.HandleResponse(ctx, nil, info) |
| } |
| |
| // GetQuestionInviteUserInfo get question invite user info |
| // @Summary get question invite user info |
| // @Description get question invite user info |
| // @Tags Question |
| // @Accept json |
| // @Produce json |
| // @Param id query string true "Question ID" default(1) |
| // @Success 200 {string} string "" |
| // @Router /answer/api/v1/question/invite [get] |
| func (qc *QuestionController) GetQuestionInviteUserInfo(ctx *gin.Context) { |
| questionID := uid.DeShortID(ctx.Query("id")) |
| resp, err := qc.questionService.InviteUserInfo(ctx, questionID) |
| handler.HandleResponse(ctx, err, resp) |
| |
| } |
| |
| // SimilarQuestion godoc |
| // @Summary Search Similar Question |
| // @Description Search Similar Question |
| // @Tags Question |
| // @Accept json |
| // @Produce json |
| // @Param question_id query string true "question_id" default() |
| // @Success 200 {string} string "" |
| // @Router /answer/api/v1/question/similar/tag [get] |
| func (qc *QuestionController) SimilarQuestion(ctx *gin.Context) { |
| questionID := ctx.Query("question_id") |
| questionID = uid.DeShortID(questionID) |
| userID := middleware.GetLoginUserIDFromContext(ctx) |
| list, count, err := qc.questionService.SimilarQuestion(ctx, questionID, userID) |
| if err != nil { |
| handler.HandleResponse(ctx, err, nil) |
| return |
| } |
| handler.HandleResponse(ctx, nil, gin.H{ |
| "list": list, |
| "count": count, |
| }) |
| } |
| |
| // QuestionPage get questions by page |
| // @Summary get questions by page |
| // @Description get questions by page |
| // @Tags Question |
| // @Accept json |
| // @Produce json |
| // @Param data body schema.QuestionPageReq true "QuestionPageReq" |
| // @Success 200 {object} handler.RespBody{data=pager.PageModel{list=[]schema.QuestionPageResp}} |
| // @Router /answer/api/v1/question/page [get] |
| func (qc *QuestionController) QuestionPage(ctx *gin.Context) { |
| req := &schema.QuestionPageReq{} |
| if handler.BindAndCheck(ctx, req) { |
| return |
| } |
| req.LoginUserID = middleware.GetLoginUserIDFromContext(ctx) |
| |
| questions, total, err := qc.questionService.GetQuestionPage(ctx, req) |
| if err != nil { |
| handler.HandleResponse(ctx, err, nil) |
| return |
| } |
| if pager.ValPageOutOfRange(total, req.Page, req.PageSize) { |
| handler.HandleResponse(ctx, errors.NotFound(reason.RequestFormatError), nil) |
| return |
| } |
| handler.HandleResponse(ctx, nil, pager.NewPageModel(total, questions)) |
| } |
| |
| // QuestionRecommendPage get recommend questions by page |
| // @Summary get recommend questions by page |
| // @Description get recommend questions by page |
| // @Tags Question |
| // @Accept json |
| // @Produce json |
| // @Param data body schema.QuestionPageReq true "QuestionPageReq" |
| // @Success 200 {object} handler.RespBody{data=pager.PageModel{list=[]schema.QuestionPageResp}} |
| // @Router /answer/api/v1/question/recommend/page [get] |
| func (qc *QuestionController) QuestionRecommendPage(ctx *gin.Context) { |
| req := &schema.QuestionPageReq{} |
| if handler.BindAndCheck(ctx, req) { |
| return |
| } |
| req.LoginUserID = middleware.GetLoginUserIDFromContext(ctx) |
| |
| if req.LoginUserID == "" { |
| handler.HandleResponse(ctx, errors.Unauthorized(reason.UnauthorizedError), nil) |
| return |
| } |
| |
| questions, total, err := qc.questionService.GetRecommendQuestionPage(ctx, req) |
| if err != nil { |
| handler.HandleResponse(ctx, err, nil) |
| return |
| } |
| handler.HandleResponse(ctx, nil, pager.NewPageModel(total, questions)) |
| } |
| |
| // AddQuestion add question |
| // @Summary add question |
| // @Description add question |
| // @Tags Question |
| // @Accept json |
| // @Produce json |
| // @Security ApiKeyAuth |
| // @Param data body schema.QuestionAdd true "question" |
| // @Success 200 {object} handler.RespBody |
| // @Router /answer/api/v1/question [post] |
| func (qc *QuestionController) AddQuestion(ctx *gin.Context) { |
| req := &schema.QuestionAdd{} |
| errFields := handler.BindAndCheckReturnErr(ctx, req) |
| if ctx.IsAborted() { |
| return |
| } |
| reject, rejectKey := qc.rateLimitMiddleware.DuplicateRequestRejection(ctx, req) |
| if reject { |
| return |
| } |
| defer func() { |
| // If status is not 200 means that the bad request has been returned, so the record should be cleared |
| if ctx.Writer.Status() != http.StatusOK { |
| qc.rateLimitMiddleware.DuplicateRequestClear(ctx, rejectKey) |
| } |
| }() |
| |
| req.UserID = middleware.GetLoginUserIDFromContext(ctx) |
| canList, requireRanks, err := qc.rankService.CheckOperationPermissionsForRanks(ctx, req.UserID, []string{ |
| permission.QuestionAdd, |
| permission.QuestionEdit, |
| permission.QuestionDelete, |
| permission.QuestionClose, |
| permission.QuestionReopen, |
| permission.TagUseReservedTag, |
| permission.TagAdd, |
| permission.LinkUrlLimit, |
| }) |
| if err != nil { |
| handler.HandleResponse(ctx, err, nil) |
| return |
| } |
| linkUrlLimitUser := canList[7] |
| isAdmin := middleware.GetUserIsAdminModerator(ctx) |
| if !isAdmin || !linkUrlLimitUser { |
| captchaPass := qc.actionService.ActionRecordVerifyCaptcha(ctx, entity.CaptchaActionQuestion, req.UserID, req.CaptchaID, req.CaptchaCode) |
| if !captchaPass { |
| errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{ |
| ErrorField: "captcha_code", |
| ErrorMsg: translator.Tr(handler.GetLang(ctx), reason.CaptchaVerificationFailed), |
| }) |
| handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields) |
| return |
| } |
| } |
| |
| req.CanAdd = canList[0] |
| req.CanEdit = canList[1] |
| req.CanDelete = canList[2] |
| req.CanClose = canList[3] |
| req.CanReopen = canList[4] |
| req.CanUseReservedTag = canList[5] |
| req.CanAddTag = canList[6] |
| if !req.CanAdd { |
| handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil) |
| return |
| } |
| |
| // can add tag |
| hasNewTag, err := qc.questionService.HasNewTag(ctx, req.Tags) |
| if err != nil { |
| handler.HandleResponse(ctx, err, nil) |
| return |
| } |
| if !req.CanAddTag && hasNewTag { |
| lang := handler.GetLang(ctx) |
| msg := translator.TrWithData(lang, reason.NoEnoughRankToOperate, &schema.PermissionTrTplData{Rank: requireRanks[6]}) |
| handler.HandleResponse(ctx, errors.Forbidden(reason.NoEnoughRankToOperate).WithMsg(msg), nil) |
| return |
| } |
| |
| errList, err := qc.questionService.CheckAddQuestion(ctx, req) |
| if err != nil { |
| errlist, ok := errList.([]*validator.FormErrorField) |
| if ok { |
| errFields = append(errFields, errlist...) |
| } |
| } |
| |
| if len(errFields) > 0 { |
| handler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), errFields) |
| return |
| } |
| |
| req.UserAgent = ctx.GetHeader("User-Agent") |
| req.IP = ctx.ClientIP() |
| |
| resp, err := qc.questionService.AddQuestion(ctx, req) |
| if err != nil { |
| errlist, ok := resp.([]*validator.FormErrorField) |
| if ok { |
| errFields = append(errFields, errlist...) |
| } |
| } |
| |
| if len(errFields) > 0 { |
| handler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), errFields) |
| return |
| } |
| if !isAdmin || !linkUrlLimitUser { |
| qc.actionService.ActionRecordAdd(ctx, entity.CaptchaActionQuestion, req.UserID) |
| } |
| handler.HandleResponse(ctx, err, resp) |
| } |
| |
| // AddQuestionByAnswer add question |
| // @Summary add question and answer |
| // @Description add question and answer |
| // @Tags Question |
| // @Accept json |
| // @Produce json |
| // @Security ApiKeyAuth |
| // @Param data body schema.QuestionAddByAnswer true "question" |
| // @Success 200 {object} handler.RespBody |
| // @Router /answer/api/v1/question/answer [post] |
| func (qc *QuestionController) AddQuestionByAnswer(ctx *gin.Context) { |
| req := &schema.QuestionAddByAnswer{} |
| errFields := handler.BindAndCheckReturnErr(ctx, req) |
| if ctx.IsAborted() { |
| return |
| } |
| req.UserID = middleware.GetLoginUserIDFromContext(ctx) |
| |
| canList, err := qc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{ |
| permission.QuestionAdd, |
| permission.QuestionEdit, |
| permission.QuestionDelete, |
| permission.QuestionClose, |
| permission.QuestionReopen, |
| permission.TagUseReservedTag, |
| permission.LinkUrlLimit, |
| }) |
| if err != nil { |
| handler.HandleResponse(ctx, err, nil) |
| return |
| } |
| |
| linkUrlLimitUser := canList[6] |
| isAdmin := middleware.GetUserIsAdminModerator(ctx) |
| if !isAdmin || !linkUrlLimitUser { |
| captchaPass := qc.actionService.ActionRecordVerifyCaptcha(ctx, entity.CaptchaActionQuestion, req.UserID, req.CaptchaID, req.CaptchaCode) |
| if !captchaPass { |
| errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{ |
| ErrorField: "captcha_code", |
| ErrorMsg: translator.Tr(handler.GetLang(ctx), reason.CaptchaVerificationFailed), |
| }) |
| handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields) |
| return |
| } |
| } |
| req.CanAdd = canList[0] |
| req.CanEdit = canList[1] |
| req.CanDelete = canList[2] |
| req.CanClose = canList[3] |
| req.CanReopen = canList[4] |
| req.CanUseReservedTag = canList[5] |
| if !req.CanAdd { |
| handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil) |
| return |
| } |
| questionReq := new(schema.QuestionAdd) |
| err = copier.Copy(questionReq, req) |
| if err != nil { |
| handler.HandleResponse(ctx, errors.Forbidden(reason.RequestFormatError), nil) |
| return |
| } |
| errList, err := qc.questionService.CheckAddQuestion(ctx, questionReq) |
| if err != nil { |
| errlist, ok := errList.([]*validator.FormErrorField) |
| if ok { |
| errFields = append(errFields, errlist...) |
| } |
| } |
| |
| if len(errFields) > 0 { |
| handler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), errFields) |
| return |
| } |
| |
| req.UserAgent = ctx.GetHeader("User-Agent") |
| req.IP = ctx.ClientIP() |
| resp, err := qc.questionService.AddQuestion(ctx, questionReq) |
| if err != nil { |
| errlist, ok := resp.([]*validator.FormErrorField) |
| if ok { |
| errFields = append(errFields, errlist...) |
| } |
| } |
| |
| if !isAdmin || !linkUrlLimitUser { |
| qc.actionService.ActionRecordAdd(ctx, entity.CaptchaActionQuestion, req.UserID) |
| } |
| |
| if len(errFields) > 0 { |
| handler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), errFields) |
| return |
| } |
| //add the question id to the answer |
| questionInfo, ok := resp.(*schema.QuestionInfoResp) |
| if ok { |
| answerReq := &schema.AnswerAddReq{} |
| answerReq.QuestionID = uid.DeShortID(questionInfo.ID) |
| answerReq.UserID = middleware.GetLoginUserIDFromContext(ctx) |
| answerReq.Content = req.AnswerContent |
| answerReq.HTML = req.AnswerHTML |
| answerID, err := qc.answerService.Insert(ctx, answerReq) |
| if err != nil { |
| handler.HandleResponse(ctx, err, nil) |
| return |
| } |
| info, questionInfo, has, err := qc.answerService.Get(ctx, answerID, req.UserID) |
| if err != nil { |
| handler.HandleResponse(ctx, err, nil) |
| return |
| } |
| if !has { |
| handler.HandleResponse(ctx, nil, nil) |
| return |
| } |
| handler.HandleResponse(ctx, err, gin.H{ |
| "info": info, |
| "question": questionInfo, |
| }) |
| return |
| } |
| |
| handler.HandleResponse(ctx, err, resp) |
| } |
| |
| // UpdateQuestion update question |
| // @Summary update question |
| // @Description update question |
| // @Tags Question |
| // @Accept json |
| // @Produce json |
| // @Security ApiKeyAuth |
| // @Param data body schema.QuestionUpdate true "question" |
| // @Success 200 {object} handler.RespBody |
| // @Router /answer/api/v1/question [put] |
| func (qc *QuestionController) UpdateQuestion(ctx *gin.Context) { |
| req := &schema.QuestionUpdate{} |
| errFields := handler.BindAndCheckReturnErr(ctx, req) |
| if ctx.IsAborted() { |
| return |
| } |
| req.ID = uid.DeShortID(req.ID) |
| req.UserID = middleware.GetLoginUserIDFromContext(ctx) |
| canList, requireRanks, err := qc.rankService.CheckOperationPermissionsForRanks(ctx, req.UserID, []string{ |
| permission.QuestionEdit, |
| permission.QuestionDelete, |
| permission.QuestionEditWithoutReview, |
| permission.TagUseReservedTag, |
| permission.TagAdd, |
| permission.LinkUrlLimit, |
| }) |
| if err != nil { |
| handler.HandleResponse(ctx, err, nil) |
| return |
| } |
| linkUrlLimitUser := canList[5] |
| isAdmin := middleware.GetUserIsAdminModerator(ctx) |
| if !isAdmin || !linkUrlLimitUser { |
| captchaPass := qc.actionService.ActionRecordVerifyCaptcha(ctx, entity.CaptchaActionEdit, req.UserID, req.CaptchaID, req.CaptchaCode) |
| if !captchaPass { |
| errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{ |
| ErrorField: "captcha_code", |
| ErrorMsg: translator.Tr(handler.GetLang(ctx), reason.CaptchaVerificationFailed), |
| }) |
| handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields) |
| return |
| } |
| } |
| |
| objectOwner := qc.rankService.CheckOperationObjectOwner(ctx, req.UserID, req.ID) |
| req.CanEdit = canList[0] || objectOwner |
| req.CanDelete = canList[1] |
| req.NoNeedReview = canList[2] || objectOwner |
| req.CanUseReservedTag = canList[3] |
| req.CanAddTag = canList[4] |
| if !req.CanEdit { |
| handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil) |
| return |
| } |
| |
| errlist, err := qc.questionService.UpdateQuestionCheckTags(ctx, req) |
| if err != nil { |
| errFields = append(errFields, errlist...) |
| } |
| |
| if len(errFields) > 0 { |
| handler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), errFields) |
| return |
| } |
| |
| // can add tag |
| hasNewTag, err := qc.questionService.HasNewTag(ctx, req.Tags) |
| if err != nil { |
| handler.HandleResponse(ctx, err, nil) |
| return |
| } |
| if !req.CanAddTag && hasNewTag { |
| lang := handler.GetLang(ctx) |
| msg := translator.TrWithData(lang, reason.NoEnoughRankToOperate, &schema.PermissionTrTplData{Rank: requireRanks[4]}) |
| handler.HandleResponse(ctx, errors.Forbidden(reason.NoEnoughRankToOperate).WithMsg(msg), nil) |
| return |
| } |
| |
| resp, err := qc.questionService.UpdateQuestion(ctx, req) |
| if err != nil { |
| handler.HandleResponse(ctx, err, resp) |
| return |
| } |
| respInfo, ok := resp.(*schema.QuestionInfoResp) |
| if !ok { |
| handler.HandleResponse(ctx, err, resp) |
| return |
| } |
| if !isAdmin || !linkUrlLimitUser { |
| qc.actionService.ActionRecordAdd(ctx, entity.CaptchaActionEdit, req.UserID) |
| } |
| handler.HandleResponse(ctx, nil, &schema.UpdateQuestionResp{UrlTitle: respInfo.UrlTitle, WaitForReview: !req.NoNeedReview}) |
| } |
| |
| // QuestionRecover recover deleted question |
| // @Summary recover deleted question |
| // @Description recover deleted question |
| // @Tags Question |
| // @Accept json |
| // @Produce json |
| // @Security ApiKeyAuth |
| // @Param data body schema.QuestionRecoverReq true "question" |
| // @Success 200 {object} handler.RespBody |
| // @Router /answer/api/v1/question/recover [post] |
| func (qc *QuestionController) QuestionRecover(ctx *gin.Context) { |
| req := &schema.QuestionRecoverReq{} |
| if handler.BindAndCheck(ctx, req) { |
| return |
| } |
| req.QuestionID = uid.DeShortID(req.QuestionID) |
| req.UserID = middleware.GetLoginUserIDFromContext(ctx) |
| |
| canList, err := qc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{ |
| permission.QuestionUnDelete, |
| }) |
| if err != nil { |
| handler.HandleResponse(ctx, err, nil) |
| return |
| } |
| if !canList[0] { |
| handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil) |
| return |
| } |
| |
| err = qc.questionService.RecoverQuestion(ctx, req) |
| handler.HandleResponse(ctx, err, nil) |
| } |
| |
| // UpdateQuestionInviteUser update question invite user |
| // @Summary update question invite user |
| // @Description update question invite user |
| // @Tags Question |
| // @Accept json |
| // @Produce json |
| // @Security ApiKeyAuth |
| // @Param data body schema.QuestionUpdateInviteUser true "question" |
| // @Success 200 {object} handler.RespBody |
| // @Router /answer/api/v1/question/invite [put] |
| func (qc *QuestionController) UpdateQuestionInviteUser(ctx *gin.Context) { |
| req := &schema.QuestionUpdateInviteUser{} |
| errFields := handler.BindAndCheckReturnErr(ctx, req) |
| if ctx.IsAborted() { |
| return |
| } |
| if len(errFields) > 0 { |
| handler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), errFields) |
| return |
| } |
| req.ID = uid.DeShortID(req.ID) |
| req.UserID = middleware.GetLoginUserIDFromContext(ctx) |
| isAdmin := middleware.GetUserIsAdminModerator(ctx) |
| if !isAdmin { |
| captchaPass := qc.actionService.ActionRecordVerifyCaptcha(ctx, entity.CaptchaActionInvitationAnswer, req.UserID, req.CaptchaID, req.CaptchaCode) |
| if !captchaPass { |
| errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{ |
| ErrorField: "captcha_code", |
| ErrorMsg: translator.Tr(handler.GetLang(ctx), reason.CaptchaVerificationFailed), |
| }) |
| handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields) |
| return |
| } |
| } |
| |
| canList, err := qc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{ |
| permission.AnswerInviteSomeoneToAnswer, |
| }) |
| if err != nil { |
| handler.HandleResponse(ctx, err, nil) |
| return |
| } |
| |
| req.CanInviteOtherToAnswer = canList[0] |
| if !req.CanInviteOtherToAnswer { |
| handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil) |
| return |
| } |
| err = qc.questionService.UpdateQuestionInviteUser(ctx, req) |
| if err != nil { |
| handler.HandleResponse(ctx, err, nil) |
| return |
| } |
| if !isAdmin { |
| qc.actionService.ActionRecordAdd(ctx, entity.CaptchaActionInvitationAnswer, req.UserID) |
| } |
| handler.HandleResponse(ctx, nil, nil) |
| } |
| |
| // GetSimilarQuestions fuzzy query similar questions based on title |
| // @Summary fuzzy query similar questions based on title |
| // @Description fuzzy query similar questions based on title |
| // @Tags Question |
| // @Accept json |
| // @Produce json |
| // @Security ApiKeyAuth |
| // @Param title query string true "title" default(string) |
| // @Success 200 {object} handler.RespBody |
| // @Router /answer/api/v1/question/similar [get] |
| func (qc *QuestionController) GetSimilarQuestions(ctx *gin.Context) { |
| title := ctx.Query("title") |
| resp, err := qc.questionService.GetQuestionsByTitle(ctx, title) |
| handler.HandleResponse(ctx, err, resp) |
| } |
| |
| // UserTop godoc |
| // @Summary UserTop |
| // @Description UserTop |
| // @Tags Question |
| // @Accept json |
| // @Produce json |
| // @Param username query string true "username" default(string) |
| // @Success 200 {object} handler.RespBody |
| // @Router /answer/api/v1/personal/qa/top [get] |
| func (qc *QuestionController) UserTop(ctx *gin.Context) { |
| userName := ctx.Query("username") |
| userID := middleware.GetLoginUserIDFromContext(ctx) |
| questionList, answerList, err := qc.questionService.SearchUserTopList(ctx, userName, userID) |
| handler.HandleResponse(ctx, err, gin.H{ |
| "question": questionList, |
| "answer": answerList, |
| }) |
| } |
| |
| // PersonalQuestionPage list personal questions |
| // @Summary list personal questions |
| // @Description list personal questions |
| // @Tags Personal |
| // @Accept json |
| // @Produce json |
| // @Security ApiKeyAuth |
| // @Param username query string true "username" default(string) |
| // @Param order query string true "order" Enums(newest,score) |
| // @Param page query string true "page" default(0) |
| // @Param page_size query string true "page_size" default(20) |
| // @Success 200 {object} handler.RespBody |
| // @Router /personal/question/page [get] |
| func (qc *QuestionController) PersonalQuestionPage(ctx *gin.Context) { |
| req := &schema.PersonalQuestionPageReq{} |
| if handler.BindAndCheck(ctx, req) { |
| return |
| } |
| |
| req.LoginUserID = middleware.GetLoginUserIDFromContext(ctx) |
| req.IsAdmin = middleware.GetUserIsAdminModerator(ctx) |
| resp, err := qc.questionService.PersonalQuestionPage(ctx, req) |
| handler.HandleResponse(ctx, err, resp) |
| } |
| |
| // PersonalAnswerPage list personal answers |
| // @Summary list personal answers |
| // @Description list personal answers |
| // @Tags Personal |
| // @Accept json |
| // @Produce json |
| // @Security ApiKeyAuth |
| // @Param username query string true "username" default(string) |
| // @Param order query string true "order" Enums(newest,score) |
| // @Param page query string true "page" default(0) |
| // @Param page_size query string true "page_size" default(20) |
| // @Success 200 {object} handler.RespBody |
| // @Router /answer/api/v1/personal/answer/page [get] |
| func (qc *QuestionController) PersonalAnswerPage(ctx *gin.Context) { |
| req := &schema.PersonalAnswerPageReq{} |
| if handler.BindAndCheck(ctx, req) { |
| return |
| } |
| |
| req.LoginUserID = middleware.GetLoginUserIDFromContext(ctx) |
| req.IsAdmin = middleware.GetUserIsAdminModerator(ctx) |
| resp, err := qc.questionService.PersonalAnswerPage(ctx, req) |
| handler.HandleResponse(ctx, err, resp) |
| } |
| |
| // PersonalCollectionPage list personal collections |
| // @Summary list personal collections |
| // @Description list personal collections |
| // @Tags Collection |
| // @Accept json |
| // @Produce json |
| // @Security ApiKeyAuth |
| // @Param page query string true "page" default(0) |
| // @Param page_size query string true "page_size" default(20) |
| // @Success 200 {object} handler.RespBody |
| // @Router /answer/api/v1/personal/collection/page [get] |
| func (qc *QuestionController) PersonalCollectionPage(ctx *gin.Context) { |
| req := &schema.PersonalCollectionPageReq{} |
| if handler.BindAndCheck(ctx, req) { |
| return |
| } |
| |
| req.UserID = middleware.GetLoginUserIDFromContext(ctx) |
| |
| resp, err := qc.questionService.PersonalCollectionPage(ctx, req) |
| handler.HandleResponse(ctx, err, resp) |
| } |
| |
| // AdminQuestionPage admin question page |
| // @Summary AdminQuestionPage admin question page |
| // @Description Status:[available,closed,deleted,pending] |
| // @Tags admin |
| // @Accept json |
| // @Produce json |
| // @Security ApiKeyAuth |
| // @Param page query int false "page size" |
| // @Param page_size query int false "page size" |
| // @Param status query string false "user status" Enums(available, closed, deleted, pending) |
| // @Param query query string false "question id or title" |
| // @Success 200 {object} handler.RespBody |
| // @Router /answer/admin/api/question/page [get] |
| func (qc *QuestionController) AdminQuestionPage(ctx *gin.Context) { |
| req := &schema.AdminQuestionPageReq{} |
| if handler.BindAndCheck(ctx, req) { |
| return |
| } |
| |
| req.LoginUserID = middleware.GetLoginUserIDFromContext(ctx) |
| resp, err := qc.questionService.AdminQuestionPage(ctx, req) |
| handler.HandleResponse(ctx, err, resp) |
| } |
| |
| // AdminAnswerPage admin answer page |
| // @Summary AdminAnswerPage admin answer page |
| // @Description Status:[available,deleted,pending] |
| // @Tags admin |
| // @Accept json |
| // @Produce json |
| // @Security ApiKeyAuth |
| // @Param page query int false "page size" |
| // @Param page_size query int false "page size" |
| // @Param status query string false "user status" Enums(available,deleted,pending) |
| // @Param query query string false "answer id or question title" |
| // @Param question_id query string false "question id" |
| // @Success 200 {object} handler.RespBody |
| // @Router /answer/admin/api/answer/page [get] |
| func (qc *QuestionController) AdminAnswerPage(ctx *gin.Context) { |
| req := &schema.AdminAnswerPageReq{} |
| if handler.BindAndCheck(ctx, req) { |
| return |
| } |
| |
| req.LoginUserID = middleware.GetLoginUserIDFromContext(ctx) |
| resp, err := qc.questionService.AdminAnswerPage(ctx, req) |
| handler.HandleResponse(ctx, err, resp) |
| } |
| |
| // AdminUpdateQuestionStatus update question status |
| // @Summary update question status |
| // @Description update question status |
| // @Tags admin |
| // @Accept json |
| // @Produce json |
| // @Security ApiKeyAuth |
| // @Param data body schema.AdminUpdateQuestionStatusReq true "AdminUpdateQuestionStatusReq" |
| // @Success 200 {object} handler.RespBody |
| // @Router /answer/admin/api/question/status [put] |
| func (qc *QuestionController) AdminUpdateQuestionStatus(ctx *gin.Context) { |
| req := &schema.AdminUpdateQuestionStatusReq{} |
| if handler.BindAndCheck(ctx, req) { |
| return |
| } |
| req.QuestionID = uid.DeShortID(req.QuestionID) |
| req.UserID = middleware.GetLoginUserIDFromContext(ctx) |
| |
| err := qc.questionService.AdminSetQuestionStatus(ctx, req) |
| handler.HandleResponse(ctx, err, nil) |
| } |
| |
| // GetQuestionLink get question link |
| // @Summary get question link |
| // @Description get question link |
| // @Tags Question |
| // @Param data query schema.GetQuestionLinkReq true "GetQuestionLinkReq" |
| // @Success 200 {object} handler.RespBody{data=pager.PageModel{list=[]schema.QuestionPageResp}} |
| // @Router /answer/api/v1/question/link [get] |
| func (qc *QuestionController) GetQuestionLink(ctx *gin.Context) { |
| req := &schema.GetQuestionLinkReq{} |
| if handler.BindAndCheck(ctx, req) { |
| return |
| } |
| req.LoginUserID = middleware.GetLoginUserIDFromContext(ctx) |
| req.QuestionID = uid.DeShortID(req.QuestionID) |
| questions, total, err := qc.questionService.GetQuestionLink(ctx, req) |
| if err != nil { |
| handler.HandleResponse(ctx, err, nil) |
| return |
| } |
| handler.HandleResponse(ctx, nil, pager.NewPageModel(total, questions)) |
| } |