diff --git a/cmd/wire_gen.go b/cmd/wire_gen.go index 940c0dc1..d18355e3 100644 --- a/cmd/wire_gen.go +++ b/cmd/wire_gen.go @@ -195,12 +195,13 @@ 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, activityQueueService) answerActivityRepo := activity.NewAnswerActivityRepo(dataData, activityRepo, userRankRepo, notificationQueueService) answerActivityService := activity2.NewAnswerActivityService(answerActivityRepo, configService) externalNotificationService := notification.NewExternalNotificationService(dataData, userNotificationConfigRepo, followRepo, emailService, userRepo, externalNotificationQueueService, userExternalLoginRepo, siteInfoCommonService) reviewRepo := review.NewReviewRepo(dataData) reviewService := review2.NewReviewService(reviewRepo, objService, userCommon, userRepo, questionRepo, answerRepo, userRoleRelService, externalNotificationQueueService, tagCommonService, questionCommon, notificationQueueService, siteInfoCommonService) - questionService := content.NewQuestionService(questionRepo, answerRepo, tagCommonService, questionCommon, userCommon, userRepo, userRoleRelService, revisionService, metaCommonService, collectionCommon, answerActivityService, emailService, notificationQueueService, externalNotificationQueueService, activityQueueService, siteInfoCommonService, externalNotificationService, reviewService, configService, eventQueueService) + questionService := content.NewQuestionService(activityRepo, questionRepo, answerRepo, tagCommonService, tagService, questionCommon, userCommon, userRepo, userRoleRelService, revisionService, metaCommonService, collectionCommon, answerActivityService, emailService, notificationQueueService, externalNotificationQueueService, activityQueueService, siteInfoCommonService, externalNotificationService, reviewService, configService, eventQueueService) answerService := content.NewAnswerService(answerRepo, questionRepo, questionCommon, userCommon, collectionCommon, userRepo, revisionService, answerActivityService, answerCommon, voteRepo, emailService, userRoleRelService, notificationQueueService, externalNotificationQueueService, activityQueueService, reviewService, eventQueueService) reportHandle := report_handle.NewReportHandle(questionService, answerService, commentService) reportService := report2.NewReportService(reportRepo, objService, userCommon, answerRepo, questionRepo, commentCommonRepo, reportHandle, configService, eventQueueService) @@ -208,7 +209,6 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database, contentVoteRepo := activity.NewVoteRepo(dataData, activityRepo, userRankRepo, notificationQueueService) voteService := content.NewVoteService(contentVoteRepo, configService, questionRepo, answerRepo, commentCommonRepo, objService, eventQueueService) voteController := controller.NewVoteController(voteService, rankService, captchaService) - tagService := tag2.NewTagService(tagRepo, tagCommonService, revisionService, followRepo, siteInfoCommonService, activityQueueService) tagController := controller.NewTagController(tagService, tagCommonService, rankService) followFollowRepo := activity.NewFollowRepo(dataData, uniqueIDRepo, activityRepo) followService := follow.NewFollowService(followFollowRepo, followRepo, tagCommonRepo) diff --git a/docs/docs.go b/docs/docs.go index a9d56ab9..7f5bd871 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -4562,6 +4562,67 @@ const docTemplate = `{ } } }, + "/answer/api/v1/question/recommend/page": { + "get": { + "description": "get recommend questions by page", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Question" + ], + "summary": "get recommend questions by page", + "parameters": [ + { + "description": "QuestionPageReq", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.QuestionPageReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "allOf": [ + { + "$ref": "#/definitions/pager.PageModel" + }, + { + "type": "object", + "properties": { + "list": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.QuestionPageResp" + } + } + } + } + ] + } + } + } + ] + } + } + } + } + }, "/answer/api/v1/question/recover": { "post": { "security": [ @@ -9491,7 +9552,8 @@ const docTemplate = `{ "active", "hot", "score", - "unanswered" + "unanswered", + "recommend" ] }, "page": { diff --git a/docs/swagger.json b/docs/swagger.json index 82ec1a6f..093e0dfc 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -4536,6 +4536,67 @@ } } }, + "/answer/api/v1/question/recommend/page": { + "get": { + "description": "get recommend questions by page", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Question" + ], + "summary": "get recommend questions by page", + "parameters": [ + { + "description": "QuestionPageReq", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.QuestionPageReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "allOf": [ + { + "$ref": "#/definitions/pager.PageModel" + }, + { + "type": "object", + "properties": { + "list": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.QuestionPageResp" + } + } + } + } + ] + } + } + } + ] + } + } + } + } + }, "/answer/api/v1/question/recover": { "post": { "security": [ @@ -9465,7 +9526,8 @@ "active", "hot", "score", - "unanswered" + "unanswered", + "recommend" ] }, "page": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 2af2900f..6d96c8a1 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1555,6 +1555,7 @@ definitions: - hot - score - unanswered + - recommend type: string page: minimum: 1 @@ -5643,6 +5644,40 @@ paths: summary: get questions by page tags: - Question + /answer/api/v1/question/recommend/page: + get: + consumes: + - application/json + description: get recommend questions by page + parameters: + - description: QuestionPageReq + in: body + name: data + required: true + schema: + $ref: '#/definitions/schema.QuestionPageReq' + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/handler.RespBody' + - properties: + data: + allOf: + - $ref: '#/definitions/pager.PageModel' + - properties: + list: + items: + $ref: '#/definitions/schema.QuestionPageResp' + type: array + type: object + type: object + summary: get recommend questions by page + tags: + - Question /answer/api/v1/question/recover: post: consumes: diff --git a/go.mod b/go.mod index 036a2b42..e14b01b9 100644 --- a/go.mod +++ b/go.mod @@ -79,7 +79,6 @@ require ( github.com/cenkalti/backoff/v4 v4.2.1 // indirect github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect github.com/containerd/continuity v0.4.2 // indirect - github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/docker/cli v24.0.6+incompatible // indirect github.com/docker/docker v24.0.6+incompatible // indirect @@ -129,7 +128,6 @@ require ( github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect - github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/spf13/afero v1.9.2 // indirect github.com/spf13/cast v1.5.0 // indirect @@ -142,13 +140,12 @@ require ( github.com/tidwall/pretty v1.2.0 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.11 // indirect - github.com/urfave/cli/v2 v2.3.0 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xeipuuv/gojsonschema v1.2.0 // indirect go.uber.org/atomic v1.10.0 // indirect - go.uber.org/multierr v1.8.0 // indirect - go.uber.org/zap v1.23.0 // indirect + go.uber.org/multierr v1.10.0 // indirect + go.uber.org/zap v1.24.0 // indirect golang.org/x/arch v0.3.0 // indirect golang.org/x/mod v0.12.0 // indirect golang.org/x/sys v0.18.0 // indirect diff --git a/go.sum b/go.sum index fb49b88b..4bf5cc08 100644 --- a/go.sum +++ b/go.sum @@ -128,7 +128,6 @@ github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7 github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= @@ -616,7 +615,6 @@ github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= @@ -692,8 +690,6 @@ github.com/swaggo/files v1.0.0/go.mod h1:N59U6URJLyU1PQgFqPM7wXLMhJx7QAolnvfQkqO github.com/swaggo/gin-swagger v1.5.3 h1:8mWmHLolIbrhJJTflsaFoZzRBYVmEE7JZGIq08EiC0Q= github.com/swaggo/gin-swagger v1.5.3/go.mod h1:3XJKSfHjDMB5dBo/0rrTXidPmgLeqsX89Yp4uA50HpI= github.com/swaggo/swag v1.8.1/go.mod h1:ugemnJsPZm/kRwFUnzBlbHRd0JY9zE1M4F+uy2pAaPQ= -github.com/swaggo/swag v1.16.1 h1:fTNRhKstPKxcnoKsytm4sahr8FaYzUcT7i1/3nd/fBg= -github.com/swaggo/swag v1.16.1/go.mod h1:9/LMvHycG3NFHfR6LwvikHv5iFvmPADQ359cKikGxto= github.com/swaggo/swag v1.16.3 h1:PnCYjPCah8FK4I26l2F/KQ4yz3sILcVUN3cTlBFA9Pg= github.com/swaggo/swag v1.16.3/go.mod h1:DImHIuOFXKpMFAQjcC7FG4m3Dg4+QuUgUzJmKjI/gRk= github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE= @@ -714,9 +710,7 @@ github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95 github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= -github.com/urfave/cli v1.22.1 h1:+mkCCcOFKPnCmVYVcURKps1Xe+3zP90gSYGNfRkjoIY= github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= -github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M= github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= @@ -752,21 +746,20 @@ go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= -go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= -go.uber.org/multierr v1.8.0 h1:dg6GjLku4EH+249NNmoIciG9N/jURbDG+pFlTkhzIC8= -go.uber.org/multierr v1.8.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= +go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= +go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= -go.uber.org/zap v1.23.0 h1:OjGQ5KQDEUawVHxNwQgPpiypGHOxo2mNZsOqTak4fFY= -go.uber.org/zap v1.23.0/go.mod h1:D+nX8jyLsMHMYrln8A0rJjFt/T/9/bGgIhAqxv5URuY= +go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= +go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= diff --git a/i18n/en_US.yaml b/i18n/en_US.yaml index f7c16e64..70318a38 100644 --- a/i18n/en_US.yaml +++ b/i18n/en_US.yaml @@ -1558,6 +1558,7 @@ ui: newest: Newest active: Active hot: Hot + recommend: Recommend score: Score unanswered: Unanswered modified: modified diff --git a/i18n/no_NO.yaml b/i18n/no_NO.yaml index c91f56b0..859f1cea 100644 --- a/i18n/no_NO.yaml +++ b/i18n/no_NO.yaml @@ -859,6 +859,7 @@ ui: newest: Newest active: Active hot: Hot + recommend: Recommend score: Score unanswered: Unanswered modified: modified diff --git a/internal/controller/question_controller.go b/internal/controller/question_controller.go index 1b0dee92..5ddc33f3 100644 --- a/internal/controller/question_controller.go +++ b/internal/controller/question_controller.go @@ -340,6 +340,35 @@ func (qc *QuestionController) QuestionPage(ctx *gin.Context) { 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 diff --git a/internal/repo/activity_common/activity_repo.go b/internal/repo/activity_common/activity_repo.go index 70f49962..9f0a59e4 100644 --- a/internal/repo/activity_common/activity_repo.go +++ b/internal/repo/activity_common/activity_repo.go @@ -107,6 +107,19 @@ func (ar *ActivityRepo) GetActivity(ctx context.Context, session *xorm.Session, return } +func (ar *ActivityRepo) GetUserActivitysByActivityType(ctx context.Context, userID string, activityType int) ( + activityList []*entity.Activity, err error) { + activityList = make([]*entity.Activity, 0) + err = ar.data.DB.Context(ctx).Where("user_id = ?", userID). + And("activity_type = ?", activityType). + And("cancelled = 0"). + Find(&activityList) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return +} + func (ar *ActivityRepo) GetUserIDObjectIDActivitySum(ctx context.Context, userID, objectID string) (int, error) { sum := &entity.ActivityRankSum{} _, err := ar.data.DB.Context(ctx).Table(entity.Activity{}.TableName()). diff --git a/internal/repo/question/question_repo.go b/internal/repo/question/question_repo.go index 83a84157..2c6e595a 100644 --- a/internal/repo/question/question_repo.go +++ b/internal/repo/question/question_repo.go @@ -401,6 +401,61 @@ func (qr *questionRepo) GetQuestionPage(ctx context.Context, page, pageSize int, return questionList, total, err } +// GetRecommendQuestionPageByTags get recommend question page by tags +func (qr *questionRepo) GetRecommendQuestionPageByTags(ctx context.Context, userID string, tagIDs, followedQuestionIDs []string, page, pageSize int) ( + questionList []*entity.Question, total int64, err error) { + questionList = make([]*entity.Question, 0) + orderBySQL := "question.pin DESC, question.created_at DESC" + + // Please Make sure every question has at least one tag + selectSQL := entity.Question{}.TableName() + ".*" + if len(followedQuestionIDs) > 0 { + idStr := "'" + strings.Join(followedQuestionIDs, "','") + "'" + selectSQL += fmt.Sprintf(", CASE WHEN question.id IN (%s) THEN 0 ELSE 1 END AS order_priority", idStr) + orderBySQL = "order_priority, " + orderBySQL + } + session := qr.data.DB.Context(ctx).Select(selectSQL) + + if len(tagIDs) > 0 { + session.Where("question.user_id != ?", userID). + And("question.id NOT IN (SELECT question_id FROM answer WHERE user_id = ?)", userID). + Join("INNER", "tag_rel", "question.id = tag_rel.object_id"). + And("tag_rel.status = ?", entity.TagRelStatusAvailable). + Join("INNER", "tag", "tag.id = tag_rel.tag_id"). + In("tag.id", tagIDs) + } else if len(followedQuestionIDs) == 0 { + return questionList, 0, nil + } + + if len(followedQuestionIDs) > 0 { + if len(tagIDs) > 0 { + // if tags provided, show followed questions and tag questions + session.Or(builder.In("question.id", followedQuestionIDs)) + } else { + // if no tags, only show followed questions + session.Where(builder.In("question.id", followedQuestionIDs)) + } + } + + session. + And("question.show = ? and question.status = ?", entity.QuestionShow, entity.QuestionStatusAvailable). + Distinct("question.id"). + OrderBy(orderBySQL) + + total, err = pager.Help(page, pageSize, &questionList, &entity.Question{}, session) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + + if handler.GetEnableShortID(ctx) { + for _, item := range questionList { + item.ID = uid.EnShortID(item.ID) + } + } + + return questionList, total, err +} + func (qr *questionRepo) AdminQuestionPage(ctx context.Context, search *schema.AdminQuestionPageReq) ([]*entity.Question, int64, error) { var ( count int64 diff --git a/internal/repo/repo_test/recommend_test.go b/internal/repo/repo_test/recommend_test.go new file mode 100644 index 00000000..6c87dfe7 --- /dev/null +++ b/internal/repo/repo_test/recommend_test.go @@ -0,0 +1,215 @@ +/* + * 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 repo_test + +import ( + "context" + "testing" + + "github.com/apache/incubator-answer/internal/entity" + "github.com/apache/incubator-answer/internal/repo/activity" + "github.com/apache/incubator-answer/internal/repo/activity_common" + "github.com/apache/incubator-answer/internal/repo/config" + "github.com/apache/incubator-answer/internal/repo/question" + "github.com/apache/incubator-answer/internal/repo/tag" + "github.com/apache/incubator-answer/internal/repo/tag_common" + "github.com/apache/incubator-answer/internal/repo/unique" + "github.com/apache/incubator-answer/internal/repo/user" + config2 "github.com/apache/incubator-answer/internal/service/config" + "github.com/stretchr/testify/assert" +) + +func Test_questionRepo_GetRecommend(t *testing.T) { + var ( + uniqueIDRepo = unique.NewUniqueIDRepo(testDataSource) + questionRepo = question.NewQuestionRepo(testDataSource, uniqueIDRepo) + userRepo = user.NewUserRepo(testDataSource) + tagRelRepo = tag.NewTagRelRepo(testDataSource, uniqueIDRepo) + tagRepo = tag.NewTagRepo(testDataSource, uniqueIDRepo) + tagCommenRepo = tag_common.NewTagCommonRepo(testDataSource, uniqueIDRepo) + configRepo = config.NewConfigRepo(testDataSource) + configService = config2.NewConfigService(configRepo) + activityCommonRepo = activity_common.NewActivityRepo(testDataSource, uniqueIDRepo, configService) + followRepo = activity.NewFollowRepo(testDataSource, uniqueIDRepo, activityCommonRepo) + ) + + // create question and user + user := &entity.User{ + Username: "ferrischi201", + Pass: "ferrischi201", + EMail: "ferrischi201@example.com", + MailStatus: entity.EmailStatusAvailable, + Status: entity.UserStatusAvailable, + DisplayName: "ferrischi201", + IsAdmin: false, + } + err := userRepo.AddUser(context.TODO(), user) + assert.NoError(t, err) + assert.NotEqual(t, "", user.ID) + + questions := make([]*entity.Question, 0) + // tag, unjoin, unfollow + questions = append(questions, &entity.Question{ + UserID: "1", + Title: "Valid recommendation 1", + OriginalText: "A go question", + ParsedText: "Go question", + Status: entity.QuestionStatusAvailable, + Show: entity.QuestionShow, + }) + // tag, unjoin, follow + questions = append(questions, &entity.Question{ + UserID: "1", + Title: "Valid recommendation 2", + OriginalText: "A go question", + ParsedText: "Go question", + Status: entity.QuestionStatusAvailable, + Show: entity.QuestionShow, + }) + // tag, join, unfollow + questions = append(questions, &entity.Question{ + UserID: user.ID, + Title: "Invalid recommendation 1", + OriginalText: "A go question 1", + ParsedText: "Go question", + Status: entity.QuestionStatusAvailable, + Show: entity.QuestionShow, + }) + // tag, join, follow + questions = append(questions, &entity.Question{ + UserID: user.ID, + Title: "Valid recommendation 3", + OriginalText: "A java question", + ParsedText: "Java question", + Status: entity.QuestionStatusAvailable, + Show: entity.QuestionShow, + }) + // untag, unjoin, unfollow + questions = append(questions, &entity.Question{ + UserID: "1", + Title: "Invalid recommendation 2", + OriginalText: "A go question", + ParsedText: "Go question", + Status: entity.QuestionStatusAvailable, + Show: entity.QuestionShow, + }) + // untag, unjoin, follow + questions = append(questions, &entity.Question{ + UserID: "1", + Title: "Valid recommendation 4", + OriginalText: "A go question", + ParsedText: "Go question", + Status: entity.QuestionStatusAvailable, + Show: entity.QuestionShow, + }) + // untag, join, unfollow + questions = append(questions, &entity.Question{ + UserID: user.ID, + Title: "Invalid recommendation 3", + OriginalText: "A go question 1", + ParsedText: "Go question", + Status: entity.QuestionStatusAvailable, + Show: entity.QuestionShow, + }) + // untag, join, follow + questions = append(questions, &entity.Question{ + UserID: user.ID, + Title: "Valid recommendation 5", + OriginalText: "A java question", + ParsedText: "Java question", + Status: entity.QuestionStatusAvailable, + Show: entity.QuestionShow, + }) + + for _, question := range questions { + err = questionRepo.AddQuestion(context.TODO(), question) + assert.NoError(t, err) + assert.NotEqual(t, "", question.ID) + } + + tags := []*entity.Tag{ + { + SlugName: "go", + DisplayName: "Golang", + OriginalText: "golang", + ParsedText: "
golang
", + Status: entity.TagStatusAvailable, + }, + { + SlugName: "java", + DisplayName: "Java", + OriginalText: "java", + ParsedText: "java
", + Status: entity.TagStatusAvailable, + }, + } + err = tagCommenRepo.AddTagList(context.TODO(), tags) + assert.NoError(t, err) + + tagRels := make([]*entity.TagRel, 0) + for i, question := range questions { + tagRel := &entity.TagRel{ + TagID: tags[i/4].ID, + ObjectID: question.ID, + Status: entity.TagRelStatusAvailable, + } + tagRels = append(tagRels, tagRel) + } + err = tagRelRepo.AddTagRelList(context.TODO(), tagRels) + assert.NoError(t, err) + + followQuestionIDs := make([]string, 0) + for i := range questions { + if i%2 == 0 { + continue + } + err = followRepo.Follow(context.TODO(), questions[i].ID, user.ID) + assert.NoError(t, err) + followQuestionIDs = append(followQuestionIDs, questions[i].ID) + } + + // get recommend + questionList, total, err := questionRepo.GetRecommendQuestionPageByTags(context.TODO(), user.ID, []string{tags[0].ID}, followQuestionIDs, 1, 20) + assert.NoError(t, err) + assert.Equal(t, int64(5), total) + assert.Equal(t, 5, len(questionList)) + + // recovery + t.Cleanup(func() { + tagRelIDs := make([]int64, 0) + for i, tagRel := range tagRels { + if i%2 == 1 { + err = followRepo.FollowCancel(context.TODO(), questions[i].ID, user.ID) + assert.NoError(t, err) + } + tagRelIDs = append(tagRelIDs, tagRel.ID) + } + err = tagRelRepo.RemoveTagRelListByIDs(context.TODO(), tagRelIDs) + assert.NoError(t, err) + for _, tag := range tags { + err = tagRepo.RemoveTag(context.TODO(), tag.ID) + assert.NoError(t, err) + } + for _, q := range questions { + err = questionRepo.RemoveQuestion(context.TODO(), q.ID) + assert.NoError(t, err) + } + }) +} diff --git a/internal/router/answer_api_router.go b/internal/router/answer_api_router.go index b4ec3bdc..cca997bd 100644 --- a/internal/router/answer_api_router.go +++ b/internal/router/answer_api_router.go @@ -165,6 +165,7 @@ func (a *AnswerAPIRouter) RegisterUnAuthAnswerAPIRouter(r *gin.RouterGroup) { r.GET("/question/info", a.questionController.GetQuestion) r.GET("/question/invite", a.questionController.GetQuestionInviteUserInfo) r.GET("/question/page", a.questionController.QuestionPage) + r.GET("/question/recommend/page", a.questionController.QuestionRecommendPage) r.GET("/question/similar/tag", a.questionController.SimilarQuestion) r.GET("/personal/qa/top", a.questionController.UserTop) r.GET("/personal/question/page", a.questionController.PersonalQuestionPage) diff --git a/internal/schema/question_schema.go b/internal/schema/question_schema.go index a4c6ee8c..2a6e5f12 100644 --- a/internal/schema/question_schema.go +++ b/internal/schema/question_schema.go @@ -355,7 +355,7 @@ const ( type QuestionPageReq struct { Page int `validate:"omitempty,min=1" form:"page"` PageSize int `validate:"omitempty,min=1" form:"page_size"` - OrderCond string `validate:"omitempty,oneof=newest active hot score unanswered" form:"order"` + OrderCond string `validate:"omitempty,oneof=newest active hot score unanswered recommend" form:"order"` Tag string `validate:"omitempty,gt=0,lte=100" form:"tag"` Username string `validate:"omitempty,gt=0,lte=100" form:"username"` InDays int `validate:"omitempty,min=1" form:"in_days"` diff --git a/internal/service/activity_common/activity.go b/internal/service/activity_common/activity.go index db565f1e..6b3557c0 100644 --- a/internal/service/activity_common/activity.go +++ b/internal/service/activity_common/activity.go @@ -37,6 +37,7 @@ type ActivityRepo interface { GetActivityTypeByObjectType(ctx context.Context, objectKey, action string) (activityType int, err error) GetActivity(ctx context.Context, session *xorm.Session, objectID, userID string, activityType int) ( existsActivity *entity.Activity, exist bool, err error) + GetUserActivitysByActivityType(ctx context.Context, userID string, activityType int) (activityList []*entity.Activity, err error) GetUserIDObjectIDActivitySum(ctx context.Context, userID, objectID string) (int, error) GetActivityTypeByConfigKey(ctx context.Context, configKey string) (activityType int, err error) AddActivity(ctx context.Context, activity *entity.Activity) (err error) diff --git a/internal/service/content/question_service.go b/internal/service/content/question_service.go index dfb778ea..d3e952b7 100644 --- a/internal/service/content/question_service.go +++ b/internal/service/content/question_service.go @@ -22,7 +22,6 @@ package content import ( "encoding/json" "fmt" - answercommon "github.com/apache/incubator-answer/internal/service/answer_common" "github.com/apache/incubator-answer/internal/service/event_queue" "strings" "time" @@ -36,11 +35,13 @@ import ( "github.com/apache/incubator-answer/internal/entity" "github.com/apache/incubator-answer/internal/schema" "github.com/apache/incubator-answer/internal/service/activity" + "github.com/apache/incubator-answer/internal/service/activity_common" "github.com/apache/incubator-answer/internal/service/activity_queue" + answercommon "github.com/apache/incubator-answer/internal/service/answer_common" collectioncommon "github.com/apache/incubator-answer/internal/service/collection_common" "github.com/apache/incubator-answer/internal/service/config" "github.com/apache/incubator-answer/internal/service/export" - "github.com/apache/incubator-answer/internal/service/meta_common" + metacommon "github.com/apache/incubator-answer/internal/service/meta_common" "github.com/apache/incubator-answer/internal/service/notice_queue" "github.com/apache/incubator-answer/internal/service/notification" "github.com/apache/incubator-answer/internal/service/permission" @@ -49,6 +50,7 @@ import ( "github.com/apache/incubator-answer/internal/service/revision_common" "github.com/apache/incubator-answer/internal/service/role" "github.com/apache/incubator-answer/internal/service/siteinfo_common" + "github.com/apache/incubator-answer/internal/service/tag" tagcommon "github.com/apache/incubator-answer/internal/service/tag_common" usercommon "github.com/apache/incubator-answer/internal/service/user_common" "github.com/apache/incubator-answer/pkg/checker" @@ -66,9 +68,11 @@ import ( // QuestionService user service type QuestionService struct { + activityRepo activity_common.ActivityRepo questionRepo questioncommon.QuestionRepo answerRepo answercommon.AnswerRepo tagCommon *tagcommon.TagCommonService + tagService *tag.TagService questioncommon *questioncommon.QuestionCommon userCommon *usercommon.UserCommon userRepo usercommon.UserRepo @@ -89,9 +93,11 @@ type QuestionService struct { } func NewQuestionService( + activityRepo activity_common.ActivityRepo, questionRepo questioncommon.QuestionRepo, answerRepo answercommon.AnswerRepo, tagCommon *tagcommon.TagCommonService, + tagService *tag.TagService, questioncommon *questioncommon.QuestionCommon, userCommon *usercommon.UserCommon, userRepo usercommon.UserRepo, @@ -111,9 +117,11 @@ func NewQuestionService( eventQueueService event_queue.EventQueueService, ) *QuestionService { return &QuestionService{ + activityRepo: activityRepo, questionRepo: questionRepo, answerRepo: answerRepo, tagCommon: tagCommon, + tagService: tagService, questioncommon: questioncommon, userCommon: userCommon, userRepo: userRepo, @@ -1358,6 +1366,47 @@ func (qs *QuestionService) GetQuestionPage(ctx context.Context, req *schema.Ques return questions, total, nil } +// GetRecommendQuestionPage retrieves recommended question page based on following tags and questions. +func (qs *QuestionService) GetRecommendQuestionPage(ctx context.Context, req *schema.QuestionPageReq) ( + questions []*schema.QuestionPageResp, total int64, err error) { + followingTagsResp, err := qs.tagService.GetFollowingTags(ctx, req.LoginUserID) + if err != nil { + return nil, 0, err + } + tagIDs := make([]string, 0, len(followingTagsResp)) + for _, tag := range followingTagsResp { + tagIDs = append(tagIDs, tag.TagID) + } + + activityType, err := qs.activityRepo.GetActivityTypeByObjectType(ctx, constant.QuestionObjectType, "follow") + if err != nil { + return nil, 0, err + } + activities, err := qs.activityRepo.GetUserActivitysByActivityType(ctx, req.LoginUserID, activityType) + if err != nil { + return nil, 0, err + } + + followedQuestionIDs := make([]string, 0, len(activities)) + for _, activity := range activities { + if activity.Cancelled == entity.ActivityCancelled { + continue + } + followedQuestionIDs = append(followedQuestionIDs, activity.ObjectID) + } + questionList, total, err := qs.questionRepo.GetRecommendQuestionPageByTags(ctx, req.LoginUserID, tagIDs, followedQuestionIDs, req.Page, req.PageSize) + if err != nil { + return nil, 0, err + } + + questions, err = qs.questioncommon.FormatQuestionsPage(ctx, questionList, req.LoginUserID, "frequent") + if err != nil { + return nil, 0, err + } + + return questions, total, nil +} + func (qs *QuestionService) AdminSetQuestionStatus(ctx context.Context, req *schema.AdminUpdateQuestionStatusReq) error { setStatus, ok := entity.AdminQuestionSearchStatus[req.Status] if !ok { diff --git a/internal/service/question_common/question.go b/internal/service/question_common/question.go index b9f2fd53..5dab1214 100644 --- a/internal/service/question_common/question.go +++ b/internal/service/question_common/question.go @@ -57,6 +57,7 @@ type QuestionRepo interface { GetQuestionList(ctx context.Context, question *entity.Question) (questions []*entity.Question, err error) GetQuestionPage(ctx context.Context, page, pageSize int, tagIDs []string, userID, orderCond string, inDays int, showHidden, showPending bool) ( questionList []*entity.Question, total int64, err error) + GetRecommendQuestionPageByTags(ctx context.Context, userID string, tagIDs, followedQuestionIDs []string, page, pageSize int) (questionList []*entity.Question, total int64, err error) UpdateQuestionStatus(ctx context.Context, questionID string, status int) (err error) UpdateQuestionStatusWithOutUpdateTime(ctx context.Context, question *entity.Question) (err error) RecoverQuestion(ctx context.Context, questionID string) (err error) diff --git a/ui/src/common/interface.ts b/ui/src/common/interface.ts index a06fec28..8a933471 100644 --- a/ui/src/common/interface.ts +++ b/ui/src/common/interface.ts @@ -296,6 +296,7 @@ export interface LangsType { * @description interface for Question */ export type QuestionOrderBy = + | 'recommend' | 'newest' | 'active' | 'hot' diff --git a/ui/src/components/QuestionList/index.tsx b/ui/src/components/QuestionList/index.tsx index d9b46dd2..c41f4b9d 100644 --- a/ui/src/components/QuestionList/index.tsx +++ b/ui/src/components/QuestionList/index.tsx @@ -43,11 +43,13 @@ export const QUESTION_ORDER_KEYS: Type.QuestionOrderBy[] = [ 'hot', 'score', 'unanswered', + 'recommend', ]; interface Props { source: 'questions' | 'tag'; order?: Type.QuestionOrderBy; data; + orderList?: Type.QuestionOrderBy[]; isLoading: boolean; } @@ -55,6 +57,7 @@ const QuestionList: FC