Merge branch 'dev' into test

# Conflicts:
#	cmd/wire_gen.go
#	internal/service/content/question_service.go
This commit is contained in:
LinkinStars
2024-09-04 10:27:58 +08:00
23 changed files with 571 additions and 27 deletions
+2 -2
View File
@@ -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)
+63 -1
View File
@@ -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": {
+63 -1
View File
@@ -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": {
+35
View File
@@ -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:
+2 -5
View File
@@ -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
+4 -11
View File
@@ -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=
+1
View File
@@ -1558,6 +1558,7 @@ ui:
newest: Newest
active: Active
hot: Hot
recommend: Recommend
score: Score
unanswered: Unanswered
modified: modified
+1
View File
@@ -859,6 +859,7 @@ ui:
newest: Newest
active: Active
hot: Hot
recommend: Recommend
score: Score
unanswered: Unanswered
modified: modified
@@ -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
@@ -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()).
+55
View File
@@ -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
+215
View File
@@ -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: "<p>golang</p>",
Status: entity.TagStatusAvailable,
},
{
SlugName: "java",
DisplayName: "Java",
OriginalText: "java",
ParsedText: "<p>java</p>",
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)
}
})
}
+1
View File
@@ -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)
+1 -1
View File
@@ -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"`
@@ -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)
+51 -2
View File
@@ -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 {
@@ -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)
+1
View File
@@ -296,6 +296,7 @@ export interface LangsType {
* @description interface for Question
*/
export type QuestionOrderBy =
| 'recommend'
| 'newest'
| 'active'
| 'hot'
+5 -1
View File
@@ -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<Props> = ({
source,
order,
data,
orderList,
isLoading = false,
}) => {
const { t } = useTranslation('translation', { keyPrefix: 'question' });
@@ -65,6 +68,7 @@ const QuestionList: FC<Props> = ({
const curPage = Number(urlSearchParams.get('page')) || 1;
const pageSize = 20;
const count = data?.count || 0;
const orderKeys = orderList || QUESTION_ORDER_KEYS;
return (
<div>
@@ -75,7 +79,7 @@ const QuestionList: FC<Props> = ({
: t('x_questions', { count })}
</h5>
<QueryGroup
data={QUESTION_ORDER_KEYS}
data={orderKeys}
currentSort={curOrder}
pathname={source === 'questions' ? '/questions' : ''}
i18nKeyPrefix="question"
@@ -110,7 +110,7 @@ const Index: FC<Props> = ({
return (
<div
className={classNames('d-block d-md-flex flex-wrap', {
className={classNames('d-flex flex-wrap', {
'mb-3': !showAddCommentBtn,
})}>
{showAddCommentBtn && (
+10 -2
View File
@@ -34,7 +34,7 @@ import {
loggedUserInfoStore,
loginSettingStore,
} from '@/stores';
import { useQuestionList } from '@/services';
import { useQuestionList, useQuestionRecommendList } from '@/services';
import * as Type from '@/common/interface';
import { userCenter, floppyNavigation } from '@/utils';
import { QUESTION_ORDER_KEYS } from '@/components/QuestionList';
@@ -52,7 +52,10 @@ const Questions: FC = () => {
page: curPage,
order: curOrder as Type.QuestionOrderBy,
};
const { data: listData, isLoading: listLoading } = useQuestionList(reqParams);
const { data: listData, isLoading: listLoading } =
curOrder === 'recommend'
? useQuestionRecommendList(reqParams)
: useQuestionList(reqParams);
const isIndexPage = useMatch('/');
let pageTitle = t('questions', { keyPrefix: 'page_title' });
let slogan = '';
@@ -71,6 +74,11 @@ const Questions: FC = () => {
source="questions"
data={listData}
order={curOrder}
orderList={
loggedUser.username
? QUESTION_ORDER_KEYS
: QUESTION_ORDER_KEYS.filter((key) => key !== 'recommend')
}
isLoading={listLoading}
/>
</Col>
+1
View File
@@ -176,6 +176,7 @@ const Index: FC = () => {
source="tag"
data={listData}
order={curOrder}
orderList={QUESTION_ORDER_KEYS.slice(0, 4)}
isLoading={listLoading}
/>
</Col>
+15
View File
@@ -36,6 +36,21 @@ export const useQuestionList = (params: Type.QueryQuestionsReq) => {
};
};
export const useQuestionRecommendList = (params: Type.QueryQuestionsReq) => {
const apiUrl = `/answer/api/v1/question/recommend/page?${qs.stringify(
params,
)}`;
const { data, error } = useSWR<Type.ListResult, Error>(
[apiUrl],
request.instance.get,
);
return {
data,
isLoading: !data && !error,
error,
};
};
export const useHotQuestions = (
params: Type.QueryQuestionsReq = {
page: 1,