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}}:
\nI 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,