diff --git a/.vscode/extensions.json b/.vscode/extensions.json deleted file mode 100644 index 28d95959..00000000 --- a/.vscode/extensions.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "recommendations": [ - "github.copilot" - ] -} diff --git a/.vscode/settings.json b/.vscode/settings.json index 6c384d1e..93106f18 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,6 +1,5 @@ { "eslint.workingDirectories": [ "ui" - ], - "commentTranslate.multiLineMerge": true + ] } diff --git a/cmd/wire_gen.go b/cmd/wire_gen.go index e88de43f..f50f34a2 100644 --- a/cmd/wire_gen.go +++ b/cmd/wire_gen.go @@ -170,7 +170,7 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database, answerActivityRepo := activity.NewAnswerActivityRepo(dataData, activityRepo, userRankRepo) questionActivityRepo := activity.NewQuestionActivityRepo(dataData, activityRepo, userRankRepo) answerActivityService := activity2.NewAnswerActivityService(answerActivityRepo, questionActivityRepo) - questionService := service.NewQuestionService(questionRepo, tagCommonService, questionCommon, userCommon, revisionService, metaService, collectionCommon, answerActivityService, dataData) + questionService := service.NewQuestionService(questionRepo, tagCommonService, questionCommon, userCommon, userRepo, revisionService, metaService, collectionCommon, answerActivityService, dataData, emailService) answerService := service.NewAnswerService(answerRepo, questionRepo, questionCommon, userCommon, collectionCommon, userRepo, revisionService, answerActivityService, answerCommon, voteRepo, emailService, userRoleRelService) questionController := controller.NewQuestionController(questionService, answerService, rankService) dashboardService := dashboard.NewDashboardService(questionRepo, answerRepo, commentCommonRepo, voteRepo, userRepo, reportRepo, configRepo, siteInfoCommonService, serviceConf, dataData) diff --git a/docs/docs.go b/docs/docs.go index 27da67dd..2c3d72ea 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -2841,6 +2841,19 @@ const docTemplate = `{ "name": "type", "in": "query", "required": true + }, + { + "enum": [ + "all", + "posts", + "invites", + "votes" + ], + "type": "string", + "description": "inbox_type", + "name": "inbox_type", + "in": "query", + "required": true } ], "responses": { @@ -3626,6 +3639,81 @@ const docTemplate = `{ } } }, + "/answer/api/v1/question/invite": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "get question invite user info", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Question" + ], + "summary": "get question invite user info", + "parameters": [ + { + "type": "string", + "default": "1", + "description": "Question ID", + "name": "id", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "string" + } + } + } + }, + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "update question invite user", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Question" + ], + "summary": "update question invite user", + "parameters": [ + { + "description": "question", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.QuestionUpdateInviteUser" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, "/answer/api/v1/question/operation": { "put": { "security": [ @@ -5025,6 +5113,55 @@ const docTemplate = `{ } } }, + "/answer/api/v1/user/info/search": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "SearchUserListByName", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "SearchUserListByName", + "parameters": [ + { + "type": "string", + "description": "username", + "name": "username", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/schema.GetOtherUserInfoResp" + } + } + } + ] + } + } + } + } + }, "/answer/api/v1/user/interface": { "put": { "security": [ @@ -7511,6 +7648,12 @@ const docTemplate = `{ "maxLength": 65535, "minLength": 6 }, + "mention_username_list": { + "type": "array", + "items": { + "type": "string" + } + }, "tags": { "description": "tags", "type": "array", @@ -7674,6 +7817,12 @@ const docTemplate = `{ "description": "question id", "type": "string" }, + "invite_user": { + "type": "array", + "items": { + "type": "string" + } + }, "tags": { "description": "tags", "type": "array", @@ -7689,6 +7838,23 @@ const docTemplate = `{ } } }, + "schema.QuestionUpdateInviteUser": { + "type": "object", + "required": [ + "id" + ], + "properties": { + "id": { + "type": "string" + }, + "invite_user": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, "schema.RemoveAnswerReq": { "type": "object", "required": [ diff --git a/docs/swagger.json b/docs/swagger.json index c54cf2af..a9cb60df 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -2829,6 +2829,19 @@ "name": "type", "in": "query", "required": true + }, + { + "enum": [ + "all", + "posts", + "invites", + "votes" + ], + "type": "string", + "description": "inbox_type", + "name": "inbox_type", + "in": "query", + "required": true } ], "responses": { @@ -3614,6 +3627,81 @@ } } }, + "/answer/api/v1/question/invite": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "get question invite user info", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Question" + ], + "summary": "get question invite user info", + "parameters": [ + { + "type": "string", + "default": "1", + "description": "Question ID", + "name": "id", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "string" + } + } + } + }, + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "update question invite user", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Question" + ], + "summary": "update question invite user", + "parameters": [ + { + "description": "question", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.QuestionUpdateInviteUser" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, "/answer/api/v1/question/operation": { "put": { "security": [ @@ -5013,6 +5101,55 @@ } } }, + "/answer/api/v1/user/info/search": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "SearchUserListByName", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "SearchUserListByName", + "parameters": [ + { + "type": "string", + "description": "username", + "name": "username", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/schema.GetOtherUserInfoResp" + } + } + } + ] + } + } + } + } + }, "/answer/api/v1/user/interface": { "put": { "security": [ @@ -7499,6 +7636,12 @@ "maxLength": 65535, "minLength": 6 }, + "mention_username_list": { + "type": "array", + "items": { + "type": "string" + } + }, "tags": { "description": "tags", "type": "array", @@ -7662,6 +7805,12 @@ "description": "question id", "type": "string" }, + "invite_user": { + "type": "array", + "items": { + "type": "string" + } + }, "tags": { "description": "tags", "type": "array", @@ -7677,6 +7826,23 @@ } } }, + "schema.QuestionUpdateInviteUser": { + "type": "object", + "required": [ + "id" + ], + "properties": { + "id": { + "type": "string" + }, + "invite_user": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, "schema.RemoveAnswerReq": { "type": "object", "required": [ diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 893f8330..7216dfd2 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1197,6 +1197,10 @@ definitions: maxLength: 65535 minLength: 6 type: string + mention_username_list: + items: + type: string + type: array tags: description: tags items: @@ -1313,6 +1317,10 @@ definitions: id: description: question id type: string + invite_user: + items: + type: string + type: array tags: description: tags items: @@ -1329,6 +1337,17 @@ definitions: - tags - title type: object + schema.QuestionUpdateInviteUser: + properties: + id: + type: string + invite_user: + items: + type: string + type: array + required: + - id + type: object schema.RemoveAnswerReq: properties: id: @@ -3957,6 +3976,16 @@ paths: name: type required: true type: string + - description: inbox_type + enum: + - all + - posts + - invites + - votes + in: query + name: inbox_type + required: true + type: string produces: - application/json responses: @@ -4439,6 +4468,53 @@ paths: summary: get question details tags: - Question + /answer/api/v1/question/invite: + get: + consumes: + - application/json + description: get question invite user info + parameters: + - default: "1" + description: Question ID + in: query + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + type: string + security: + - ApiKeyAuth: [] + summary: get question invite user info + tags: + - Question + put: + consumes: + - application/json + description: update question invite user + parameters: + - description: question + in: body + name: data + required: true + schema: + $ref: '#/definitions/schema.QuestionUpdateInviteUser' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handler.RespBody' + security: + - ApiKeyAuth: [] + summary: update question invite user + tags: + - Question /answer/api/v1/question/operation: put: consumes: @@ -5291,6 +5367,34 @@ paths: summary: UserUpdateInfo update user info tags: - User + /answer/api/v1/user/info/search: + get: + consumes: + - application/json + description: SearchUserListByName + parameters: + - description: username + in: query + name: username + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/handler.RespBody' + - properties: + data: + $ref: '#/definitions/schema.GetOtherUserInfoResp' + type: object + security: + - ApiKeyAuth: [] + summary: SearchUserListByName + tags: + - User /answer/api/v1/user/interface: put: consumes: diff --git a/i18n/en_US.yaml b/i18n/en_US.yaml index 1068fbb0..8bae17ed 100644 --- a/i18n/en_US.yaml +++ b/i18n/en_US.yaml @@ -388,6 +388,8 @@ backend: other: downvoted answer up_voted_comment: other: upvoted comment + invited_you_to_answer: + other: invited you to answer email_tpl: change_email: title: @@ -399,6 +401,11 @@ backend: other: "[{{.SiteName}}] {{.DisplayName}} answered your question" body: other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.AnswerSummary}}

\nView it on {{.SiteName}}

\n\nYou are receiving this because you authored the thread. Unsubscribe" + invited_you_to_answer: + title: + other: "[{{.SiteName}}] {{.DisplayName}} invited you to answer" + body: + other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
I think you may know the answer.

\nView it on {{.SiteName}}

\n\nYou are receiving this because you authored the thread. Unsubscribe" new_comment: title: other: "[{{.SiteName}}] {{.DisplayName}} commented on your post" diff --git a/i18n/zh_CN.yaml b/i18n/zh_CN.yaml index ca2edf49..6329718e 100644 --- a/i18n/zh_CN.yaml +++ b/i18n/zh_CN.yaml @@ -378,6 +378,8 @@ backend: other: 踩了答案 up_voted_comment: other: 赞了评论 + invited_you_to_answer: + other: 邀请你回答问题 email_tpl: change_email: title: @@ -388,12 +390,17 @@ backend: title: other: "[{{.SiteName}}] {{.DisplayName}} 回答了您的问题" body: - other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.AnswerSummary}}

\n在 {{.SiteName}} 上查看

\n\n您会收到此邮件是因为您是该讨论的作者。取消订阅" + other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.AnswerSummary}}

\n在 {{.SiteName}} 上查看

\n\n您会收到此邮件是因为您开启了订阅。取消订阅" + invited_you_to_answer: + title: + other: "[{{.SiteName}}] {{.DisplayName}} 邀请您回答问题" + body: + other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
我想你可能知道答案。

\n在 {{.SiteName}} 上查看

\n\n您会收到此邮件是因为您开启了订阅. 取消订阅" new_comment: title: other: "[{{.SiteName}}] {{.DisplayName}} 评论了您的帖子" body: - other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.CommentSummary}}

\n在 {{.SiteName}} 上查看

\n\n您会收到此邮件是因为您是该讨论的作者。取消订阅" + other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.CommentSummary}}

\n在 {{.SiteName}} 上查看

\n\n您会收到此邮件是因为您开启了订阅。取消订阅" pass_reset: title: other: "[{{.SiteName }}] 重置密码" diff --git a/internal/base/constant/email_tpl_key.go b/internal/base/constant/email_tpl_key.go index 178f2d30..cb191165 100644 --- a/internal/base/constant/email_tpl_key.go +++ b/internal/base/constant/email_tpl_key.go @@ -18,4 +18,7 @@ const ( EmailTplKeyTestTitle = "email_tpl.test.title" EmailTplKeyTestBody = "email_tpl.test.body" + + EmailTplKeyInvitedAnswerTitle = "email_tpl.invited_you_to_answer.title" + EmailTplKeyInvitedAnswerBody = "email_tpl.invited_you_to_answer.body" ) diff --git a/internal/base/constant/notification.go b/internal/base/constant/notification.go index 8f8568a8..165c4c01 100644 --- a/internal/base/constant/notification.go +++ b/internal/base/constant/notification.go @@ -35,4 +35,6 @@ const ( NotificationYourAnswerWasDeleted = "notification.action.your_answer_was_deleted" // NotificationYourCommentWasDeleted your comment was deleted NotificationYourCommentWasDeleted = "notification.action.your_comment_was_deleted" + // NotificationInvitedYouToAnswer invited you to answer + NotificationInvitedYouToAnswer = "notification.action.invited_you_to_answer" ) diff --git a/internal/controller/connector_controller.go b/internal/controller/connector_controller.go index bb6c37ad..a4ba035b 100644 --- a/internal/controller/connector_controller.go +++ b/internal/controller/connector_controller.go @@ -109,7 +109,7 @@ func (cc *ConnectorController) ConnectorRedirect(connector plugin.Connector) (fn commonRouterPrefix, ConnectorRedirectRouterPrefix, connector.ConnectorSlugName()) userInfo, err := connector.ConnectorReceiver(ctx, receiverURL) if err != nil { - log.Errorf("connector received failed: %v", err) + log.Errorf("connector received failed, error info: %v, response data is: %s", err, userInfo.MetaInfo) ctx.Redirect(http.StatusFound, "/50x") return } diff --git a/internal/controller/notification_controller.go b/internal/controller/notification_controller.go index f5b88238..7206fead 100644 --- a/internal/controller/notification_controller.go +++ b/internal/controller/notification_controller.go @@ -143,6 +143,7 @@ func (nc *NotificationController) ClearIDUnRead(ctx *gin.Context) { // @Param page query int false "page size" // @Param page_size query int false "page size" // @Param type query string true "type" Enums(inbox,achievement) +// @Param inbox_type query string true "inbox_type" Enums(all,posts,invites,votes) // @Success 200 {object} handler.RespBody // @Router /answer/api/v1/notification/page [get] func (nc *NotificationController) GetList(ctx *gin.Context) { diff --git a/internal/controller/question_controller.go b/internal/controller/question_controller.go index 72d3df00..77e85535 100644 --- a/internal/controller/question_controller.go +++ b/internal/controller/question_controller.go @@ -221,6 +221,23 @@ func (qc *QuestionController) GetQuestion(ctx *gin.Context) { 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 +// @Security ApiKeyAuth +// @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 @@ -500,6 +517,51 @@ func (qc *QuestionController) UpdateQuestion(ctx *gin.Context) { handler.HandleResponse(ctx, nil, &schema.UpdateQuestionResp{WaitForReview: !req.NoNeedReview}) } +// 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 + } + req.ID = uid.DeShortID(req.ID) + req.UserID = middleware.GetLoginUserIDFromContext(ctx) + + canList, err := qc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{ + permission.AnswerInviteSomeoneToAnswer, + }) + if err != nil { + handler.HandleResponse(ctx, err, nil) + return + } + + objectOwner := qc.rankService.CheckOperationObjectOwner(ctx, req.UserID, req.ID) + req.CanEdit = canList[0] || objectOwner + if !req.CanEdit { + handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil) + return + } + if len(errFields) > 0 { + handler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), errFields) + return + } + err = qc.questionService.UpdateQuestionInviteUser(ctx, req) + if err != nil { + handler.HandleResponse(ctx, err, nil) + return + } + handler.HandleResponse(ctx, nil, nil) +} + // SearchByTitleLike add question title like // @Summary add question title like // @Description add question title like diff --git a/internal/controller/user_controller.go b/internal/controller/user_controller.go index 5266e66b..4fbf0482 100644 --- a/internal/controller/user_controller.go +++ b/internal/controller/user_controller.go @@ -607,3 +607,22 @@ func (uc *UserController) UserUnsubscribeEmailNotification(ctx *gin.Context) { err := uc.userService.UserUnsubscribeEmailNotification(ctx, req) handler.HandleResponse(ctx, err, nil) } + +// SearchUserListByName godoc +// @Summary SearchUserListByName +// @Description SearchUserListByName +// @Tags User +// @Accept json +// @Produce json +// @Security ApiKeyAuth +// @Param username query string true "username" +// @Success 200 {object} handler.RespBody{data=schema.GetOtherUserInfoResp} +// @Router /answer/api/v1/user/info/search [get] +func (uc *UserController) SearchUserListByName(ctx *gin.Context) { + req := &schema.GetOtherUserInfoByUsernameReq{} + if handler.BindAndCheck(ctx, req) { + return + } + resp, err := uc.userService.SearchUserListByName(ctx, req.Username) + handler.HandleResponse(ctx, err, resp) +} diff --git a/internal/entity/question_entity.go b/internal/entity/question_entity.go index 41c9236b..bfcc24ef 100644 --- a/internal/entity/question_entity.go +++ b/internal/entity/question_entity.go @@ -32,6 +32,7 @@ type Question struct { CreatedAt time.Time `xorm:"not null default CURRENT_TIMESTAMP TIMESTAMP created_at"` UpdatedAt time.Time `xorm:"updated_at TIMESTAMP"` UserID string `xorm:"not null default 0 BIGINT(20) INDEX user_id"` + InviteUserID string `xorm:"TEXT invite_user_id"` LastEditUserID string `xorm:"not null default 0 BIGINT(20) last_edit_user_id"` Title string `xorm:"not null default '' VARCHAR(150) title"` OriginalText string `xorm:"not null MEDIUMTEXT original_text"` diff --git a/internal/migrations/v12.go b/internal/migrations/v12.go index 34b6c0c4..2107228b 100644 --- a/internal/migrations/v12.go +++ b/internal/migrations/v12.go @@ -2,14 +2,44 @@ package migrations import ( "fmt" + "time" "github.com/answerdev/answer/internal/entity" "github.com/segmentfault/pacman/log" "xorm.io/xorm" ) +type QuestionPostTime struct { + ID string `xorm:"not null pk BIGINT(20) id"` + CreatedAt time.Time `xorm:"not null default CURRENT_TIMESTAMP TIMESTAMP created_at"` + UpdatedAt time.Time `xorm:"updated_at TIMESTAMP"` + UserID string `xorm:"not null default 0 BIGINT(20) INDEX user_id"` + InviteUserID string `xorm:"TEXT invite_user_id"` + LastEditUserID string `xorm:"not null default 0 BIGINT(20) last_edit_user_id"` + Title string `xorm:"not null default '' VARCHAR(150) title"` + OriginalText string `xorm:"not null MEDIUMTEXT original_text"` + ParsedText string `xorm:"not null MEDIUMTEXT parsed_text"` + Status int `xorm:"not null default 1 INT(11) status"` + Pin int `xorm:"not null default 1 INT(11) pin"` + Show int `xorm:"not null default 1 INT(11) show"` + ViewCount int `xorm:"not null default 0 INT(11) view_count"` + UniqueViewCount int `xorm:"not null default 0 INT(11) unique_view_count"` + VoteCount int `xorm:"not null default 0 INT(11) vote_count"` + AnswerCount int `xorm:"not null default 0 INT(11) answer_count"` + CollectionCount int `xorm:"not null default 0 INT(11) collection_count"` + FollowCount int `xorm:"not null default 0 INT(11) follow_count"` + AcceptedAnswerID string `xorm:"not null default 0 BIGINT(20) accepted_answer_id"` + LastAnswerID string `xorm:"not null default 0 BIGINT(20) last_answer_id"` + PostUpdateTime time.Time `xorm:"post_update_time TIMESTAMP"` + RevisionID string `xorm:"not null default 0 BIGINT(20) revision_id"` +} + +func (QuestionPostTime) TableName() string { + return "question" +} + func updateQuestionPostTime(x *xorm.Engine) error { - questionList := make([]entity.Question, 0) + questionList := make([]QuestionPostTime, 0) err := x.Find(&questionList, &entity.Question{}) if err != nil { return fmt.Errorf("get questions failed: %w", err) @@ -21,7 +51,7 @@ func updateQuestionPostTime(x *xorm.Engine) error { } else if !item.CreatedAt.IsZero() { item.PostUpdateTime = item.CreatedAt } - if _, err = x.Update(item, &entity.Question{ID: item.ID}); err != nil { + if _, err = x.Update(item, &QuestionPostTime{ID: item.ID}); err != nil { log.Errorf("update %+v config failed: %s", item, err) return fmt.Errorf("update question failed: %w", err) } diff --git a/internal/migrations/v13.go b/internal/migrations/v13.go index e3f75bb0..877469b2 100644 --- a/internal/migrations/v13.go +++ b/internal/migrations/v13.go @@ -3,12 +3,13 @@ package migrations import ( "encoding/json" "fmt" + "time" "github.com/answerdev/answer/internal/base/constant" "github.com/answerdev/answer/internal/entity" "github.com/answerdev/answer/internal/schema" - "github.com/segmentfault/pacman/log" "github.com/answerdev/answer/internal/service/permission" + "github.com/segmentfault/pacman/log" "xorm.io/xorm" ) @@ -19,6 +20,7 @@ func updateCount(x *xorm.Engine) error { updateTagCount(x) updateUserQuestionCount(x) updateUserAnswerCount(x) + inviteAnswer(x) return nil } @@ -302,3 +304,36 @@ func updateUserAnswerCount(x *xorm.Engine) error { } return nil } + +func inviteAnswer(x *xorm.Engine) error { + type Question struct { + ID string `xorm:"not null pk BIGINT(20) id"` + CreatedAt time.Time `xorm:"not null default CURRENT_TIMESTAMP TIMESTAMP created_at"` + UpdatedAt time.Time `xorm:"updated_at TIMESTAMP"` + UserID string `xorm:"not null default 0 BIGINT(20) INDEX user_id"` + InviteUserID string `xorm:"TEXT invite_user_id"` + LastEditUserID string `xorm:"not null default 0 BIGINT(20) last_edit_user_id"` + Title string `xorm:"not null default '' VARCHAR(150) title"` + OriginalText string `xorm:"not null MEDIUMTEXT original_text"` + ParsedText string `xorm:"not null MEDIUMTEXT parsed_text"` + Status int `xorm:"not null default 1 INT(11) status"` + Pin int `xorm:"not null default 1 INT(11) pin"` + Show int `xorm:"not null default 1 INT(11) show"` + ViewCount int `xorm:"not null default 0 INT(11) view_count"` + UniqueViewCount int `xorm:"not null default 0 INT(11) unique_view_count"` + VoteCount int `xorm:"not null default 0 INT(11) vote_count"` + AnswerCount int `xorm:"not null default 0 INT(11) answer_count"` + CollectionCount int `xorm:"not null default 0 INT(11) collection_count"` + FollowCount int `xorm:"not null default 0 INT(11) follow_count"` + AcceptedAnswerID string `xorm:"not null default 0 BIGINT(20) accepted_answer_id"` + LastAnswerID string `xorm:"not null default 0 BIGINT(20) last_answer_id"` + PostUpdateTime time.Time `xorm:"post_update_time TIMESTAMP"` + RevisionID string `xorm:"not null default 0 BIGINT(20) revision_id"` + } + err := x.Sync(new(Question)) + if err != nil { + return err + } + + return nil +} diff --git a/internal/repo/user/user_repo.go b/internal/repo/user/user_repo.go index 53c46ca0..97be3837 100644 --- a/internal/repo/user/user_repo.go +++ b/internal/repo/user/user_repo.go @@ -195,6 +195,17 @@ func (ur *userRepo) GetByUsername(ctx context.Context, username string) (userInf return } +func (ur *userRepo) GetByUsernames(ctx context.Context, usernames []string) ([]*entity.User, error) { + list := make([]*entity.User, 0) + err := ur.data.DB.Where("status =?", entity.UserStatusAvailable).In("username", usernames).Find(&list) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + return list, err + } + tryToDecorateUserListFromUserCenter(ctx, ur.data, list) + return list, nil +} + // GetByEmail get user by email func (ur *userRepo) GetByEmail(ctx context.Context, email string) (userInfo *entity.User, exist bool, err error) { userInfo = &entity.User{} @@ -215,6 +226,23 @@ func (ur *userRepo) GetUserCount(ctx context.Context) (count int64, err error) { return } +func (ur *userRepo) SearchUserListByName(ctx context.Context, name string) (userList []*entity.User, err error) { + userList = make([]*entity.User, 0) + if name == "" { + return userList, nil + } + session := ur.data.DB.Where("") + session.Where("username LIKE LOWER(?) or display_name LIKE ?", name+"%", name+"%").And("status =?", entity.UserStatusAvailable) + session.Asc("username") + session = session.Limit(5, 0) + err = session.OrderBy("id desc").Find(&userList) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + tryToDecorateUserListFromUserCenter(ctx, ur.data, userList) + return +} + func tryToDecorateUserInfoFromUserCenter(ctx context.Context, data *data.Data, original *entity.User) (err error) { if original == nil { return nil diff --git a/internal/router/answer_api_router.go b/internal/router/answer_api_router.go index a7c2dab9..ccc46909 100644 --- a/internal/router/answer_api_router.go +++ b/internal/router/answer_api_router.go @@ -112,6 +112,7 @@ func (a *AnswerAPIRouter) RegisterMustUnAuthAnswerAPIRouter(r *gin.RouterGroup) routerGroup.POST("/user/password/reset", a.userController.RetrievePassWord) routerGroup.POST("/user/password/replacement", a.userController.UseRePassWord) routerGroup.PUT("/user/email/notification", a.userController.UserUnsubscribeEmailNotification) + routerGroup.GET("/user/info/search", a.userController.SearchUserListByName) } func (a *AnswerAPIRouter) RegisterUnAuthAnswerAPIRouter(r *gin.RouterGroup) { @@ -129,6 +130,7 @@ func (a *AnswerAPIRouter) RegisterUnAuthAnswerAPIRouter(r *gin.RouterGroup) { //question r.GET("/question/info", a.questionController.GetQuestion) + r.GET("/question/invite", a.questionController.GetQuestionInviteUserInfo) r.GET("/question/page", a.questionController.QuestionPage) r.GET("/question/similar/tag", a.questionController.SimilarQuestion) r.GET("/personal/qa/top", a.questionController.UserTop) @@ -193,6 +195,7 @@ func (a *AnswerAPIRouter) RegisterAnswerAPIRouter(r *gin.RouterGroup) { r.POST("/question", a.questionController.AddQuestion) r.POST("/question/answer", a.questionController.AddQuestionByAnswer) r.PUT("/question", a.questionController.UpdateQuestion) + r.PUT("/question/invite", a.questionController.UpdateQuestionInviteUser) r.DELETE("/question", a.questionController.RemoveQuestion) r.PUT("/question/status", a.questionController.CloseQuestion) r.PUT("/question/operation", a.questionController.OperationQuestion) diff --git a/internal/schema/email_template.go b/internal/schema/email_template.go index 94918331..01cc1667 100644 --- a/internal/schema/email_template.go +++ b/internal/schema/email_template.go @@ -47,6 +47,21 @@ type NewAnswerTemplateData struct { UnsubscribeUrl string } +type NewInviteAnswerTemplateRawData struct { + InviterDisplayName string + QuestionTitle string + QuestionID string + UnsubscribeCode string +} + +type NewInviteAnswerTemplateData struct { + SiteName string + DisplayName string + QuestionTitle string + InviteUrl string + UnsubscribeUrl string +} + type NewCommentTemplateRawData struct { CommentUserDisplayName string QuestionTitle string diff --git a/internal/schema/notification_schema.go b/internal/schema/notification_schema.go index 8688af37..fd9569ca 100644 --- a/internal/schema/notification_schema.go +++ b/internal/schema/notification_schema.go @@ -1,12 +1,16 @@ package schema const ( - NotificationTypeInbox = 1 - NotificationTypeAchievement = 2 - NotificationNotRead = 1 - NotificationRead = 2 - NotificationStatusNormal = 1 - NotificationStatusDelete = 10 + NotificationTypeInbox = 1 + NotificationTypeAchievement = 2 + NotificationNotRead = 1 + NotificationRead = 2 + NotificationStatusNormal = 1 + NotificationStatusDelete = 10 + NotificationInboxTypeAll = 1 + NotificationInboxTypePosts = 2 + NotificationInboxTypeInvites = 3 + NotificationInboxTypeVotes = 4 ) var NotificationType = map[string]int{ @@ -14,6 +18,13 @@ var NotificationType = map[string]int{ "achievement": NotificationTypeAchievement, } +var NotificationInboxType = map[string]int{ + "all": NotificationInboxTypeAll, + "posts": NotificationInboxTypePosts, + "invites": NotificationInboxTypeInvites, + "votes": NotificationInboxTypeVotes, +} + type NotificationContent struct { ID string `json:"id"` TriggerUserID string `json:"-"` //show userid @@ -69,11 +80,13 @@ type RedDot struct { } type NotificationSearch struct { - Page int `json:"page" form:"page"` //Query number of pages - PageSize int `json:"page_size" form:"page_size"` //Search page size - Type int `json:"-" form:"-"` - TypeStr string `json:"type" form:"type"` // inbox achievement - UserID string `json:"-"` + Page int `json:"page" form:"page"` //Query number of pages + PageSize int `json:"page_size" form:"page_size"` //Search page size + Type int `json:"-" form:"-"` + TypeStr string `json:"type" form:"type"` // inbox achievement + InboxTypeStr string `json:"inbox_type" form:"inbox_type"` // inbox achievement + InboxType int `json:"-" form:"-"` // inbox achievement + UserID string `json:"-"` } type NotificationClearRequest struct { diff --git a/internal/schema/question_schema.go b/internal/schema/question_schema.go index 60af4f14..3f0b518c 100644 --- a/internal/schema/question_schema.go +++ b/internal/schema/question_schema.go @@ -87,7 +87,8 @@ type QuestionAddByAnswer struct { // tags Tags []*TagItem `validate:"required,dive" json:"tags"` // user id - UserID string `json:"-"` + UserID string `json:"-"` + MentionUsernameList []string `validate:"omitempty" json:"mention_username_list"` QuestionPermission } @@ -139,7 +140,8 @@ type QuestionUpdate struct { // content Content string `validate:"required,notblank,gte=6,lte=65535" json:"content"` // html - HTML string `json:"-"` + HTML string `json:"-"` + InviteUser []string `validate:"omitempty" json:"invite_user"` // tags Tags []*TagItem `validate:"required,dive" json:"tags"` // edit summary @@ -150,6 +152,13 @@ type QuestionUpdate struct { QuestionPermission } +type QuestionUpdateInviteUser struct { + ID string `validate:"required" json:"id"` + InviteUser []string `validate:"omitempty" json:"invite_user"` + UserID string `json:"-"` + QuestionPermission +} + func (req *QuestionUpdate) Check() (errFields []*validator.FormErrorField, err error) { req.HTML = converter.Markdown2HTML(req.Content) return nil, nil diff --git a/internal/service/export/email_service.go b/internal/service/export/email_service.go index 63b26598..4fd2fa9f 100644 --- a/internal/service/export/email_service.go +++ b/internal/service/export/email_service.go @@ -242,7 +242,6 @@ func (es *EmailService) NewAnswerTemplate(ctx context.Context, raw *schema.NewAn AnswerSummary: raw.AnswerSummary, UnsubscribeUrl: fmt.Sprintf("%s/users/unsubscribe?code=%s", siteInfo.SiteUrl, raw.UnsubscribeCode), } - templateData.SiteName = siteInfo.Name lang := handler.GetLangByCtx(ctx) title = translator.TrWithData(lang, constant.EmailTplKeyNewAnswerTitle, templateData) @@ -250,6 +249,27 @@ func (es *EmailService) NewAnswerTemplate(ctx context.Context, raw *schema.NewAn return title, body, nil } +// NewInviteAnswerTemplate new invite answer template +func (es *EmailService) NewInviteAnswerTemplate(ctx context.Context, raw *schema.NewInviteAnswerTemplateRawData) ( + title, body string, err error) { + siteInfo, err := es.GetSiteGeneral(ctx) + if err != nil { + return + } + templateData := &schema.NewInviteAnswerTemplateData{ + SiteName: siteInfo.Name, + DisplayName: raw.InviterDisplayName, + QuestionTitle: raw.QuestionTitle, + InviteUrl: fmt.Sprintf("%s/questions/%s", siteInfo.SiteUrl, raw.QuestionID), + UnsubscribeUrl: fmt.Sprintf("%s/users/unsubscribe?code=%s", siteInfo.SiteUrl, raw.UnsubscribeCode), + } + + lang := handler.GetLangByCtx(ctx) + title = translator.TrWithData(lang, constant.EmailTplKeyInvitedAnswerTitle, templateData) + body = translator.TrWithData(lang, constant.EmailTplKeyInvitedAnswerBody, templateData) + return title, body, nil +} + // NewCommentTemplate new comment template func (es *EmailService) NewCommentTemplate(ctx context.Context, raw *schema.NewCommentTemplateRawData) ( title, body string, err error) { @@ -271,7 +291,6 @@ func (es *EmailService) NewCommentTemplate(ctx context.Context, raw *schema.NewC templateData.CommentUrl = fmt.Sprintf("%s/questions/%s?commentId=%s", siteInfo.SiteUrl, raw.QuestionID, raw.CommentID) } - templateData.SiteName = siteInfo.Name lang := handler.GetLangByCtx(ctx) title = translator.TrWithData(lang, constant.EmailTplKeyNewCommentTitle, templateData) diff --git a/internal/service/notification/notification_service.go b/internal/service/notification/notification_service.go index 3a8cce13..3f23be3e 100644 --- a/internal/service/notification/notification_service.go +++ b/internal/service/notification/notification_service.go @@ -123,7 +123,12 @@ func (ns *NotificationService) GetNotificationPage(ctx context.Context, searchCo if !ok { return pager.NewPageModel(0, resp), nil } + searchInboxType, ok := schema.NotificationInboxType[searchCond.InboxTypeStr] + if !ok { + return pager.NewPageModel(0, resp), nil + } searchCond.Type = searchType + searchCond.InboxType = searchInboxType notifications, total, err := ns.notificationRepo.GetNotificationPage(ctx, searchCond) if err != nil { return nil, err diff --git a/internal/service/question_common/question.go b/internal/service/question_common/question.go index 46a76067..26eb2cc6 100644 --- a/internal/service/question_common/question.go +++ b/internal/service/question_common/question.go @@ -150,6 +150,34 @@ func (qs *QuestionCommon) FindInfoByID(ctx context.Context, questionIDs []string return list, nil } +func (qs *QuestionCommon) InviteUserInfo(ctx context.Context, questionID string) (inviteList []*schema.UserBasicInfo, err error) { + InviteUserInfo := make([]*schema.UserBasicInfo, 0) + dbinfo, has, err := qs.questionRepo.GetQuestion(ctx, questionID) + if err != nil { + return InviteUserInfo, err + } + if !has { + return InviteUserInfo, errors.NotFound(reason.QuestionNotFound) + } + //InviteUser + if dbinfo.InviteUserID != "" { + InviteUserIDs := make([]string, 0) + err := json.Unmarshal([]byte(dbinfo.InviteUserID), &InviteUserIDs) + if err == nil { + inviteUserInfoMap, err := qs.userCommon.BatchUserBasicInfoByID(ctx, InviteUserIDs) + if err == nil { + for _, userid := range InviteUserIDs { + _, ok := inviteUserInfoMap[userid] + if ok { + InviteUserInfo = append(InviteUserInfo, inviteUserInfoMap[userid]) + } + } + } + } + } + return InviteUserInfo, nil +} + func (qs *QuestionCommon) Info(ctx context.Context, questionID string, loginUserID string) (showinfo *schema.QuestionInfo, err error) { dbinfo, has, err := qs.questionRepo.GetQuestion(ctx, questionID) if err != nil { @@ -186,9 +214,7 @@ func (qs *QuestionCommon) Info(ctx context.Context, questionID string, loginUser operation.Level = schema.OperationLevelInfo showinfo.Operation = operation } - } - } } diff --git a/internal/service/question_service.go b/internal/service/question_service.go index ccb281f6..a2ae66ba 100644 --- a/internal/service/question_service.go +++ b/internal/service/question_service.go @@ -19,6 +19,7 @@ import ( "github.com/answerdev/answer/internal/service/activity" "github.com/answerdev/answer/internal/service/activity_queue" collectioncommon "github.com/answerdev/answer/internal/service/collection_common" + "github.com/answerdev/answer/internal/service/export" "github.com/answerdev/answer/internal/service/meta" "github.com/answerdev/answer/internal/service/notice_queue" "github.com/answerdev/answer/internal/service/permission" @@ -26,10 +27,12 @@ import ( "github.com/answerdev/answer/internal/service/revision_common" tagcommon "github.com/answerdev/answer/internal/service/tag_common" usercommon "github.com/answerdev/answer/internal/service/user_common" + "github.com/answerdev/answer/pkg/encryption" "github.com/answerdev/answer/pkg/htmltext" "github.com/answerdev/answer/pkg/uid" "github.com/jinzhu/copier" "github.com/segmentfault/pacman/errors" + "github.com/segmentfault/pacman/i18n" "github.com/segmentfault/pacman/log" "golang.org/x/net/context" ) @@ -42,11 +45,13 @@ type QuestionService struct { tagCommon *tagcommon.TagCommonService questioncommon *questioncommon.QuestionCommon userCommon *usercommon.UserCommon + userRepo usercommon.UserRepo revisionService *revision_common.RevisionService metaService *meta.MetaService collectionCommon *collectioncommon.CollectionCommon answerActivityService *activity.AnswerActivityService data *data.Data + emailService *export.EmailService } func NewQuestionService( @@ -54,23 +59,26 @@ func NewQuestionService( tagCommon *tagcommon.TagCommonService, questioncommon *questioncommon.QuestionCommon, userCommon *usercommon.UserCommon, + userRepo usercommon.UserRepo, revisionService *revision_common.RevisionService, metaService *meta.MetaService, collectionCommon *collectioncommon.CollectionCommon, answerActivityService *activity.AnswerActivityService, data *data.Data, - + emailService *export.EmailService, ) *QuestionService { return &QuestionService{ questionRepo: questionRepo, tagCommon: tagCommon, questioncommon: questioncommon, userCommon: userCommon, + userRepo: userRepo, revisionService: revisionService, metaService: metaService, collectionCommon: collectionCommon, answerActivityService: answerActivityService, data: data, + emailService: emailService, } } @@ -535,6 +543,115 @@ func (qs *QuestionService) UpdateQuestionCheckTags(ctx context.Context, req *sch return nil, nil } +func (qs *QuestionService) UpdateQuestionInviteUser(ctx context.Context, req *schema.QuestionUpdateInviteUser) (err error) { + originQuestion, exist, err := qs.questionRepo.GetQuestion(ctx, req.ID) + if err != nil { + return err + } + if !exist { + return errors.NotFound(reason.ObjectNotFound) + } + + //verify invite user + inviteUserInfoList, err := qs.userCommon.BatchGetUserBasicInfoByUserNames(ctx, req.InviteUser) + if err != nil { + log.Error("BatchGetUserBasicInfoByUserNames error", err.Error()) + } + inviteUserIDs := make([]string, 0) + for _, item := range req.InviteUser { + _, ok := inviteUserInfoList[item] + if ok { + inviteUserIDs = append(inviteUserIDs, inviteUserInfoList[item].ID) + } + } + inviteUserStr := "" + inviteUserByte, err := json.Marshal(inviteUserIDs) + if err != nil { + log.Error("json.Marshal error", err.Error()) + inviteUserStr = "[]" + } else { + inviteUserStr = string(inviteUserByte) + } + question := &entity.Question{} + question.ID = uid.DeShortID(req.ID) + question.InviteUserID = inviteUserStr + + saveerr := qs.questionRepo.UpdateQuestion(ctx, question, []string{"invite_user_id"}) + if saveerr != nil { + return saveerr + } + go qs.notificationInviteUser(ctx, inviteUserIDs, originQuestion.ID, originQuestion.Title, req.UserID) + return nil +} + +func (qs *QuestionService) notificationInviteUser( + ctx context.Context, invitedUserIDs []string, questionID, questionTitle, questionUserID string) { + inviter, exist, err := qs.userCommon.GetUserBasicInfoByID(ctx, questionUserID) + if err != nil { + log.Error(err) + return + } + if !exist { + log.Warnf("user %s not found", questionUserID) + return + } + + users, err := qs.userRepo.BatchGetByID(ctx, invitedUserIDs) + if err != nil { + log.Error(err) + return + } + invitee := make(map[string]*entity.User, len(users)) + for _, user := range users { + invitee[user.ID] = user + } + for _, userID := range invitedUserIDs { + msg := &schema.NotificationMsg{ + ReceiverUserID: userID, + TriggerUserID: questionUserID, + Type: schema.NotificationTypeInbox, + ObjectID: questionID, + } + msg.ObjectType = constant.QuestionObjectType + msg.NotificationAction = constant.NotificationInvitedYouToAnswer + notice_queue.AddNotification(msg) + + userInfo, ok := invitee[userID] + if !ok { + log.Warnf("user %s not found", userID) + return + } + if userInfo.NoticeStatus == schema.NoticeStatusOff || len(userInfo.EMail) == 0 { + return + } + + rawData := &schema.NewInviteAnswerTemplateRawData{ + InviterDisplayName: inviter.DisplayName, + QuestionTitle: questionTitle, + QuestionID: questionID, + UnsubscribeCode: encryption.MD5(userInfo.Pass), + } + codeContent := &schema.EmailCodeContent{ + SourceType: schema.UnsubscribeSourceType, + Email: userInfo.EMail, + UserID: userInfo.ID, + } + + // If receiver has set language, use it to send email. + if len(userInfo.Language) > 0 { + ctx = context.WithValue(ctx, constant.AcceptLanguageFlag, i18n.Language(userInfo.Language)) + } + title, body, err := qs.emailService.NewInviteAnswerTemplate(ctx, rawData) + if err != nil { + log.Error(err) + return + } + + go qs.emailService.SendAndSaveCodeWithTime( + ctx, userInfo.EMail, title, body, rawData.UnsubscribeCode, codeContent.ToJSONString(), 7*24*time.Hour) + } +} + // UpdateQuestion update question func (qs *QuestionService) UpdateQuestion(ctx context.Context, req *schema.QuestionUpdate) (questionInfo any, err error) { var canUpdate bool @@ -756,6 +873,10 @@ func (qs *QuestionService) GetQuestionAndAddPV(ctx context.Context, questionID, return qs.GetQuestion(ctx, questionID, loginUserID, per) } +func (qs *QuestionService) InviteUserInfo(ctx context.Context, questionID string) (inviteList []*schema.UserBasicInfo, err error) { + return qs.questioncommon.InviteUserInfo(ctx, questionID) +} + func (qs *QuestionService) ChangeTag(ctx context.Context, objectTagData *schema.TagChange) error { return qs.tagCommon.ObjectChangeTag(ctx, objectTagData) } diff --git a/internal/service/user_common/user.go b/internal/service/user_common/user.go index c5786746..b6331e70 100644 --- a/internal/service/user_common/user.go +++ b/internal/service/user_common/user.go @@ -32,8 +32,10 @@ type UserRepo interface { GetByUserID(ctx context.Context, userID string) (userInfo *entity.User, exist bool, err error) BatchGetByID(ctx context.Context, ids []string) ([]*entity.User, error) GetByUsername(ctx context.Context, username string) (userInfo *entity.User, exist bool, err error) + GetByUsernames(ctx context.Context, usernames []string) ([]*entity.User, error) GetByEmail(ctx context.Context, email string) (userInfo *entity.User, exist bool, err error) GetUserCount(ctx context.Context) (count int64, err error) + SearchUserListByName(ctx context.Context, name string) (userList []*entity.User, err error) } // UserCommon user service @@ -74,6 +76,19 @@ func (us *UserCommon) GetUserBasicInfoByUserName(ctx context.Context, username s return info, exist, nil } +func (us *UserCommon) BatchGetUserBasicInfoByUserNames(ctx context.Context, usernames []string) (map[string]*schema.UserBasicInfo, error) { + infomap := make(map[string]*schema.UserBasicInfo) + list, err := us.userRepo.GetByUsernames(ctx, usernames) + if err != nil { + return infomap, err + } + for _, user := range list { + info := us.FormatUserBasicInfo(ctx, user) + infomap[user.Username] = info + } + return infomap, nil +} + func (us *UserCommon) UpdateAnswerCount(ctx context.Context, userID string, num int) error { return us.userRepo.UpdateAnswerCount(ctx, userID, num) } diff --git a/internal/service/user_service.go b/internal/service/user_service.go index 00e6b191..c6ca63c2 100644 --- a/internal/service/user_service.go +++ b/internal/service/user_service.go @@ -814,6 +814,19 @@ func (us *UserService) getUserInfoMapping(ctx context.Context, userIDs []string) return userInfoMapping, nil } +func (us *UserService) SearchUserListByName(ctx context.Context, name string) ([]*schema.UserBasicInfo, error) { + userinfolist := make([]*schema.UserBasicInfo, 0) + list, err := us.userRepo.SearchUserListByName(ctx, name) + if err != nil { + return userinfolist, err + } + for _, user := range list { + userinfo := us.userCommonService.FormatUserBasicInfo(ctx, user) + userinfolist = append(userinfolist, userinfo) + } + return userinfolist, nil +} + func (us *UserService) warpStatRankingResp( userInfoMapping map[string]*entity.User, rankStat []*entity.ActivityUserRankStat,