Merge branch 'feat/1.1.0/report' into test

# Conflicts:
#	internal/migrations/v13.go
This commit is contained in:
LinkinStars 2023-05-24 11:20:51 +08:00
commit 8c395bc306
28 changed files with 896 additions and 32 deletions

View File

@ -1,5 +0,0 @@
{
"recommendations": [
"github.copilot"
]
}

View File

@ -1,6 +1,5 @@
{ {
"eslint.workingDirectories": [ "eslint.workingDirectories": [
"ui" "ui"
], ]
"commentTranslate.multiLineMerge": true
} }

View File

@ -170,7 +170,7 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database,
answerActivityRepo := activity.NewAnswerActivityRepo(dataData, activityRepo, userRankRepo) answerActivityRepo := activity.NewAnswerActivityRepo(dataData, activityRepo, userRankRepo)
questionActivityRepo := activity.NewQuestionActivityRepo(dataData, activityRepo, userRankRepo) questionActivityRepo := activity.NewQuestionActivityRepo(dataData, activityRepo, userRankRepo)
answerActivityService := activity2.NewAnswerActivityService(answerActivityRepo, questionActivityRepo) 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) answerService := service.NewAnswerService(answerRepo, questionRepo, questionCommon, userCommon, collectionCommon, userRepo, revisionService, answerActivityService, answerCommon, voteRepo, emailService, userRoleRelService)
questionController := controller.NewQuestionController(questionService, answerService, rankService) questionController := controller.NewQuestionController(questionService, answerService, rankService)
dashboardService := dashboard.NewDashboardService(questionRepo, answerRepo, commentCommonRepo, voteRepo, userRepo, reportRepo, configRepo, siteInfoCommonService, serviceConf, dataData) dashboardService := dashboard.NewDashboardService(questionRepo, answerRepo, commentCommonRepo, voteRepo, userRepo, reportRepo, configRepo, siteInfoCommonService, serviceConf, dataData)

View File

@ -2841,6 +2841,19 @@ const docTemplate = `{
"name": "type", "name": "type",
"in": "query", "in": "query",
"required": true "required": true
},
{
"enum": [
"all",
"posts",
"invites",
"votes"
],
"type": "string",
"description": "inbox_type",
"name": "inbox_type",
"in": "query",
"required": true
} }
], ],
"responses": { "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": { "/answer/api/v1/question/operation": {
"put": { "put": {
"security": [ "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": { "/answer/api/v1/user/interface": {
"put": { "put": {
"security": [ "security": [
@ -7511,6 +7648,12 @@ const docTemplate = `{
"maxLength": 65535, "maxLength": 65535,
"minLength": 6 "minLength": 6
}, },
"mention_username_list": {
"type": "array",
"items": {
"type": "string"
}
},
"tags": { "tags": {
"description": "tags", "description": "tags",
"type": "array", "type": "array",
@ -7674,6 +7817,12 @@ const docTemplate = `{
"description": "question id", "description": "question id",
"type": "string" "type": "string"
}, },
"invite_user": {
"type": "array",
"items": {
"type": "string"
}
},
"tags": { "tags": {
"description": "tags", "description": "tags",
"type": "array", "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": { "schema.RemoveAnswerReq": {
"type": "object", "type": "object",
"required": [ "required": [

View File

@ -2829,6 +2829,19 @@
"name": "type", "name": "type",
"in": "query", "in": "query",
"required": true "required": true
},
{
"enum": [
"all",
"posts",
"invites",
"votes"
],
"type": "string",
"description": "inbox_type",
"name": "inbox_type",
"in": "query",
"required": true
} }
], ],
"responses": { "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": { "/answer/api/v1/question/operation": {
"put": { "put": {
"security": [ "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": { "/answer/api/v1/user/interface": {
"put": { "put": {
"security": [ "security": [
@ -7499,6 +7636,12 @@
"maxLength": 65535, "maxLength": 65535,
"minLength": 6 "minLength": 6
}, },
"mention_username_list": {
"type": "array",
"items": {
"type": "string"
}
},
"tags": { "tags": {
"description": "tags", "description": "tags",
"type": "array", "type": "array",
@ -7662,6 +7805,12 @@
"description": "question id", "description": "question id",
"type": "string" "type": "string"
}, },
"invite_user": {
"type": "array",
"items": {
"type": "string"
}
},
"tags": { "tags": {
"description": "tags", "description": "tags",
"type": "array", "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": { "schema.RemoveAnswerReq": {
"type": "object", "type": "object",
"required": [ "required": [

View File

@ -1197,6 +1197,10 @@ definitions:
maxLength: 65535 maxLength: 65535
minLength: 6 minLength: 6
type: string type: string
mention_username_list:
items:
type: string
type: array
tags: tags:
description: tags description: tags
items: items:
@ -1313,6 +1317,10 @@ definitions:
id: id:
description: question id description: question id
type: string type: string
invite_user:
items:
type: string
type: array
tags: tags:
description: tags description: tags
items: items:
@ -1329,6 +1337,17 @@ definitions:
- tags - tags
- title - title
type: object type: object
schema.QuestionUpdateInviteUser:
properties:
id:
type: string
invite_user:
items:
type: string
type: array
required:
- id
type: object
schema.RemoveAnswerReq: schema.RemoveAnswerReq:
properties: properties:
id: id:
@ -3957,6 +3976,16 @@ paths:
name: type name: type
required: true required: true
type: string type: string
- description: inbox_type
enum:
- all
- posts
- invites
- votes
in: query
name: inbox_type
required: true
type: string
produces: produces:
- application/json - application/json
responses: responses:
@ -4439,6 +4468,53 @@ paths:
summary: get question details summary: get question details
tags: tags:
- Question - 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: /answer/api/v1/question/operation:
put: put:
consumes: consumes:
@ -5291,6 +5367,34 @@ paths:
summary: UserUpdateInfo update user info summary: UserUpdateInfo update user info
tags: tags:
- User - 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: /answer/api/v1/user/interface:
put: put:
consumes: consumes:

View File

@ -388,6 +388,8 @@ backend:
other: downvoted answer other: downvoted answer
up_voted_comment: up_voted_comment:
other: upvoted comment other: upvoted comment
invited_you_to_answer:
other: invited you to answer
email_tpl: email_tpl:
change_email: change_email:
title: title:
@ -399,6 +401,11 @@ backend:
other: "[{{.SiteName}}] {{.DisplayName}} answered your question" other: "[{{.SiteName}}] {{.DisplayName}} answered your question"
body: body:
other: "<strong><a href='{{.AnswerUrl}}'>{{.QuestionTitle}}</a></strong><br><br>\n\n<small>{{.DisplayName}}:</small><br>\n<blockquote>{{.AnswerSummary}}</blockquote><br>\n<a href='{{.AnswerUrl}}'>View it on {{.SiteName}}</a><br><br>\n\n<small>You are receiving this because you authored the thread. <a href='{{.UnsubscribeUrl}}'>Unsubscribe</a></small>" other: "<strong><a href='{{.AnswerUrl}}'>{{.QuestionTitle}}</a></strong><br><br>\n\n<small>{{.DisplayName}}:</small><br>\n<blockquote>{{.AnswerSummary}}</blockquote><br>\n<a href='{{.AnswerUrl}}'>View it on {{.SiteName}}</a><br><br>\n\n<small>You are receiving this because you authored the thread. <a href='{{.UnsubscribeUrl}}'>Unsubscribe</a></small>"
invited_you_to_answer:
title:
other: "[{{.SiteName}}] {{.DisplayName}} invited you to answer"
body:
other: "<strong><a href='{{.InviteUrl}}'>{{.QuestionTitle}}</a></strong><br><br>\n\n<small>{{.DisplayName}}:</small><br>\n<blockquote>I think you may know the answer.</blockquote><br>\n<a href='{{.InviteUrl}}'>View it on {{.SiteName}}</a><br><br>\n\n<small>You are receiving this because you authored the thread. <a href='{{.UnsubscribeUrl}}'>Unsubscribe</a></small>"
new_comment: new_comment:
title: title:
other: "[{{.SiteName}}] {{.DisplayName}} commented on your post" other: "[{{.SiteName}}] {{.DisplayName}} commented on your post"

View File

@ -378,6 +378,8 @@ backend:
other: 踩了答案 other: 踩了答案
up_voted_comment: up_voted_comment:
other: 赞了评论 other: 赞了评论
invited_you_to_answer:
other: 邀请你回答问题
email_tpl: email_tpl:
change_email: change_email:
title: title:
@ -388,12 +390,17 @@ backend:
title: title:
other: "[{{.SiteName}}] {{.DisplayName}} 回答了您的问题" other: "[{{.SiteName}}] {{.DisplayName}} 回答了您的问题"
body: body:
other: "<strong><a href='{{.AnswerUrl}}'>{{.QuestionTitle}}</a></strong><br><br>\n\n<small>{{.DisplayName}}:</small><br>\n<blockquote>{{.AnswerSummary}}</blockquote><br>\n<a href='{{.AnswerUrl}}'>在 {{.SiteName}} 上查看</a><br><br>\n\n<small>您会收到此邮件是因为您是该讨论的作者。<a href='{{.UnsubscribeUrl}}'>取消订阅</a></small>" other: "<strong><a href='{{.AnswerUrl}}'>{{.QuestionTitle}}</a></strong><br><br>\n\n<small>{{.DisplayName}}:</small><br>\n<blockquote>{{.AnswerSummary}}</blockquote><br>\n<a href='{{.AnswerUrl}}'>在 {{.SiteName}} 上查看</a><br><br>\n\n<small>您会收到此邮件是因为您开启了订阅。<a href='{{.UnsubscribeUrl}}'>取消订阅</a></small>"
invited_you_to_answer:
title:
other: "[{{.SiteName}}] {{.DisplayName}} 邀请您回答问题"
body:
other: "<strong><a href='{{.InviteUrl}}'>{{.QuestionTitle}}</a></strong><br><br>\n\n<small>{{.DisplayName}}:</small><br>\n<blockquote>我想你可能知道答案。</blockquote><br>\n<a href='{{.InviteUrl}}'>在 {{.SiteName}} 上查看</a><br><br>\n\n<small>您会收到此邮件是因为您开启了订阅. <a href='{{.UnsubscribeUrl}}'>取消订阅</a></small>"
new_comment: new_comment:
title: title:
other: "[{{.SiteName}}] {{.DisplayName}} 评论了您的帖子" other: "[{{.SiteName}}] {{.DisplayName}} 评论了您的帖子"
body: body:
other: "<strong><a href='{{.CommentUrl}}'>{{.QuestionTitle}}</a></strong><br><br>\n\n<small>{{.DisplayName}}:</small><br>\n<blockquote>{{.CommentSummary}}</blockquote><br>\n<a href='{{.CommentUrl}}'>在 {{.SiteName}} 上查看</a><br><br>\n\n<small>您会收到此邮件是因为您是该讨论的作者。<a href='{{.UnsubscribeUrl}}'>取消订阅</a></small>" other: "<strong><a href='{{.CommentUrl}}'>{{.QuestionTitle}}</a></strong><br><br>\n\n<small>{{.DisplayName}}:</small><br>\n<blockquote>{{.CommentSummary}}</blockquote><br>\n<a href='{{.CommentUrl}}'>在 {{.SiteName}} 上查看</a><br><br>\n\n<small>您会收到此邮件是因为您开启了订阅。<a href='{{.UnsubscribeUrl}}'>取消订阅</a></small>"
pass_reset: pass_reset:
title: title:
other: "[{{.SiteName }}] 重置密码" other: "[{{.SiteName }}] 重置密码"

View File

@ -18,4 +18,7 @@ const (
EmailTplKeyTestTitle = "email_tpl.test.title" EmailTplKeyTestTitle = "email_tpl.test.title"
EmailTplKeyTestBody = "email_tpl.test.body" EmailTplKeyTestBody = "email_tpl.test.body"
EmailTplKeyInvitedAnswerTitle = "email_tpl.invited_you_to_answer.title"
EmailTplKeyInvitedAnswerBody = "email_tpl.invited_you_to_answer.body"
) )

View File

@ -35,4 +35,6 @@ const (
NotificationYourAnswerWasDeleted = "notification.action.your_answer_was_deleted" NotificationYourAnswerWasDeleted = "notification.action.your_answer_was_deleted"
// NotificationYourCommentWasDeleted your comment was deleted // NotificationYourCommentWasDeleted your comment was deleted
NotificationYourCommentWasDeleted = "notification.action.your_comment_was_deleted" NotificationYourCommentWasDeleted = "notification.action.your_comment_was_deleted"
// NotificationInvitedYouToAnswer invited you to answer
NotificationInvitedYouToAnswer = "notification.action.invited_you_to_answer"
) )

View File

@ -109,7 +109,7 @@ func (cc *ConnectorController) ConnectorRedirect(connector plugin.Connector) (fn
commonRouterPrefix, ConnectorRedirectRouterPrefix, connector.ConnectorSlugName()) commonRouterPrefix, ConnectorRedirectRouterPrefix, connector.ConnectorSlugName())
userInfo, err := connector.ConnectorReceiver(ctx, receiverURL) userInfo, err := connector.ConnectorReceiver(ctx, receiverURL)
if err != nil { 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") ctx.Redirect(http.StatusFound, "/50x")
return return
} }

View File

@ -143,6 +143,7 @@ func (nc *NotificationController) ClearIDUnRead(ctx *gin.Context) {
// @Param page query int false "page size" // @Param page query int false "page size"
// @Param page_size query int false "page size" // @Param page_size query int false "page size"
// @Param type query string true "type" Enums(inbox,achievement) // @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 // @Success 200 {object} handler.RespBody
// @Router /answer/api/v1/notification/page [get] // @Router /answer/api/v1/notification/page [get]
func (nc *NotificationController) GetList(ctx *gin.Context) { func (nc *NotificationController) GetList(ctx *gin.Context) {

View File

@ -221,6 +221,23 @@ func (qc *QuestionController) GetQuestion(ctx *gin.Context) {
handler.HandleResponse(ctx, nil, info) 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 // SimilarQuestion godoc
// @Summary Search Similar Question // @Summary Search Similar Question
// @Description 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}) 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 // SearchByTitleLike add question title like
// @Summary add question title like // @Summary add question title like
// @Description add question title like // @Description add question title like

View File

@ -607,3 +607,22 @@ func (uc *UserController) UserUnsubscribeEmailNotification(ctx *gin.Context) {
err := uc.userService.UserUnsubscribeEmailNotification(ctx, req) err := uc.userService.UserUnsubscribeEmailNotification(ctx, req)
handler.HandleResponse(ctx, err, nil) 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)
}

View File

@ -32,6 +32,7 @@ type Question struct {
CreatedAt time.Time `xorm:"not null default CURRENT_TIMESTAMP TIMESTAMP created_at"` CreatedAt time.Time `xorm:"not null default CURRENT_TIMESTAMP TIMESTAMP created_at"`
UpdatedAt time.Time `xorm:"updated_at TIMESTAMP"` UpdatedAt time.Time `xorm:"updated_at TIMESTAMP"`
UserID string `xorm:"not null default 0 BIGINT(20) INDEX user_id"` 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"` LastEditUserID string `xorm:"not null default 0 BIGINT(20) last_edit_user_id"`
Title string `xorm:"not null default '' VARCHAR(150) title"` Title string `xorm:"not null default '' VARCHAR(150) title"`
OriginalText string `xorm:"not null MEDIUMTEXT original_text"` OriginalText string `xorm:"not null MEDIUMTEXT original_text"`

View File

@ -2,14 +2,44 @@ package migrations
import ( import (
"fmt" "fmt"
"time"
"github.com/answerdev/answer/internal/entity" "github.com/answerdev/answer/internal/entity"
"github.com/segmentfault/pacman/log" "github.com/segmentfault/pacman/log"
"xorm.io/xorm" "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 { func updateQuestionPostTime(x *xorm.Engine) error {
questionList := make([]entity.Question, 0) questionList := make([]QuestionPostTime, 0)
err := x.Find(&questionList, &entity.Question{}) err := x.Find(&questionList, &entity.Question{})
if err != nil { if err != nil {
return fmt.Errorf("get questions failed: %w", err) return fmt.Errorf("get questions failed: %w", err)
@ -21,7 +51,7 @@ func updateQuestionPostTime(x *xorm.Engine) error {
} else if !item.CreatedAt.IsZero() { } else if !item.CreatedAt.IsZero() {
item.PostUpdateTime = item.CreatedAt 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) log.Errorf("update %+v config failed: %s", item, err)
return fmt.Errorf("update question failed: %w", err) return fmt.Errorf("update question failed: %w", err)
} }

View File

@ -3,12 +3,13 @@ package migrations
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"time"
"github.com/answerdev/answer/internal/base/constant" "github.com/answerdev/answer/internal/base/constant"
"github.com/answerdev/answer/internal/entity" "github.com/answerdev/answer/internal/entity"
"github.com/answerdev/answer/internal/schema" "github.com/answerdev/answer/internal/schema"
"github.com/segmentfault/pacman/log"
"github.com/answerdev/answer/internal/service/permission" "github.com/answerdev/answer/internal/service/permission"
"github.com/segmentfault/pacman/log"
"xorm.io/xorm" "xorm.io/xorm"
) )
@ -19,6 +20,7 @@ func updateCount(x *xorm.Engine) error {
updateTagCount(x) updateTagCount(x)
updateUserQuestionCount(x) updateUserQuestionCount(x)
updateUserAnswerCount(x) updateUserAnswerCount(x)
inviteAnswer(x)
return nil return nil
} }
@ -302,3 +304,36 @@ func updateUserAnswerCount(x *xorm.Engine) error {
} }
return nil 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
}

View File

@ -195,6 +195,17 @@ func (ur *userRepo) GetByUsername(ctx context.Context, username string) (userInf
return 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 // GetByEmail get user by email
func (ur *userRepo) GetByEmail(ctx context.Context, email string) (userInfo *entity.User, exist bool, err error) { func (ur *userRepo) GetByEmail(ctx context.Context, email string) (userInfo *entity.User, exist bool, err error) {
userInfo = &entity.User{} userInfo = &entity.User{}
@ -215,6 +226,23 @@ func (ur *userRepo) GetUserCount(ctx context.Context) (count int64, err error) {
return 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) { func tryToDecorateUserInfoFromUserCenter(ctx context.Context, data *data.Data, original *entity.User) (err error) {
if original == nil { if original == nil {
return nil return nil

View File

@ -112,6 +112,7 @@ func (a *AnswerAPIRouter) RegisterMustUnAuthAnswerAPIRouter(r *gin.RouterGroup)
routerGroup.POST("/user/password/reset", a.userController.RetrievePassWord) routerGroup.POST("/user/password/reset", a.userController.RetrievePassWord)
routerGroup.POST("/user/password/replacement", a.userController.UseRePassWord) routerGroup.POST("/user/password/replacement", a.userController.UseRePassWord)
routerGroup.PUT("/user/email/notification", a.userController.UserUnsubscribeEmailNotification) routerGroup.PUT("/user/email/notification", a.userController.UserUnsubscribeEmailNotification)
routerGroup.GET("/user/info/search", a.userController.SearchUserListByName)
} }
func (a *AnswerAPIRouter) RegisterUnAuthAnswerAPIRouter(r *gin.RouterGroup) { func (a *AnswerAPIRouter) RegisterUnAuthAnswerAPIRouter(r *gin.RouterGroup) {
@ -129,6 +130,7 @@ func (a *AnswerAPIRouter) RegisterUnAuthAnswerAPIRouter(r *gin.RouterGroup) {
//question //question
r.GET("/question/info", a.questionController.GetQuestion) r.GET("/question/info", a.questionController.GetQuestion)
r.GET("/question/invite", a.questionController.GetQuestionInviteUserInfo)
r.GET("/question/page", a.questionController.QuestionPage) r.GET("/question/page", a.questionController.QuestionPage)
r.GET("/question/similar/tag", a.questionController.SimilarQuestion) r.GET("/question/similar/tag", a.questionController.SimilarQuestion)
r.GET("/personal/qa/top", a.questionController.UserTop) 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", a.questionController.AddQuestion)
r.POST("/question/answer", a.questionController.AddQuestionByAnswer) r.POST("/question/answer", a.questionController.AddQuestionByAnswer)
r.PUT("/question", a.questionController.UpdateQuestion) r.PUT("/question", a.questionController.UpdateQuestion)
r.PUT("/question/invite", a.questionController.UpdateQuestionInviteUser)
r.DELETE("/question", a.questionController.RemoveQuestion) r.DELETE("/question", a.questionController.RemoveQuestion)
r.PUT("/question/status", a.questionController.CloseQuestion) r.PUT("/question/status", a.questionController.CloseQuestion)
r.PUT("/question/operation", a.questionController.OperationQuestion) r.PUT("/question/operation", a.questionController.OperationQuestion)

View File

@ -47,6 +47,21 @@ type NewAnswerTemplateData struct {
UnsubscribeUrl string 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 { type NewCommentTemplateRawData struct {
CommentUserDisplayName string CommentUserDisplayName string
QuestionTitle string QuestionTitle string

View File

@ -1,12 +1,16 @@
package schema package schema
const ( const (
NotificationTypeInbox = 1 NotificationTypeInbox = 1
NotificationTypeAchievement = 2 NotificationTypeAchievement = 2
NotificationNotRead = 1 NotificationNotRead = 1
NotificationRead = 2 NotificationRead = 2
NotificationStatusNormal = 1 NotificationStatusNormal = 1
NotificationStatusDelete = 10 NotificationStatusDelete = 10
NotificationInboxTypeAll = 1
NotificationInboxTypePosts = 2
NotificationInboxTypeInvites = 3
NotificationInboxTypeVotes = 4
) )
var NotificationType = map[string]int{ var NotificationType = map[string]int{
@ -14,6 +18,13 @@ var NotificationType = map[string]int{
"achievement": NotificationTypeAchievement, "achievement": NotificationTypeAchievement,
} }
var NotificationInboxType = map[string]int{
"all": NotificationInboxTypeAll,
"posts": NotificationInboxTypePosts,
"invites": NotificationInboxTypeInvites,
"votes": NotificationInboxTypeVotes,
}
type NotificationContent struct { type NotificationContent struct {
ID string `json:"id"` ID string `json:"id"`
TriggerUserID string `json:"-"` //show userid TriggerUserID string `json:"-"` //show userid
@ -69,11 +80,13 @@ type RedDot struct {
} }
type NotificationSearch struct { type NotificationSearch struct {
Page int `json:"page" form:"page"` //Query number of pages Page int `json:"page" form:"page"` //Query number of pages
PageSize int `json:"page_size" form:"page_size"` //Search page size PageSize int `json:"page_size" form:"page_size"` //Search page size
Type int `json:"-" form:"-"` Type int `json:"-" form:"-"`
TypeStr string `json:"type" form:"type"` // inbox achievement TypeStr string `json:"type" form:"type"` // inbox achievement
UserID string `json:"-"` InboxTypeStr string `json:"inbox_type" form:"inbox_type"` // inbox achievement
InboxType int `json:"-" form:"-"` // inbox achievement
UserID string `json:"-"`
} }
type NotificationClearRequest struct { type NotificationClearRequest struct {

View File

@ -87,7 +87,8 @@ type QuestionAddByAnswer struct {
// tags // tags
Tags []*TagItem `validate:"required,dive" json:"tags"` Tags []*TagItem `validate:"required,dive" json:"tags"`
// user id // user id
UserID string `json:"-"` UserID string `json:"-"`
MentionUsernameList []string `validate:"omitempty" json:"mention_username_list"`
QuestionPermission QuestionPermission
} }
@ -139,7 +140,8 @@ type QuestionUpdate struct {
// content // content
Content string `validate:"required,notblank,gte=6,lte=65535" json:"content"` Content string `validate:"required,notblank,gte=6,lte=65535" json:"content"`
// html // html
HTML string `json:"-"` HTML string `json:"-"`
InviteUser []string `validate:"omitempty" json:"invite_user"`
// tags // tags
Tags []*TagItem `validate:"required,dive" json:"tags"` Tags []*TagItem `validate:"required,dive" json:"tags"`
// edit summary // edit summary
@ -150,6 +152,13 @@ type QuestionUpdate struct {
QuestionPermission 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) { func (req *QuestionUpdate) Check() (errFields []*validator.FormErrorField, err error) {
req.HTML = converter.Markdown2HTML(req.Content) req.HTML = converter.Markdown2HTML(req.Content)
return nil, nil return nil, nil

View File

@ -242,7 +242,6 @@ func (es *EmailService) NewAnswerTemplate(ctx context.Context, raw *schema.NewAn
AnswerSummary: raw.AnswerSummary, AnswerSummary: raw.AnswerSummary,
UnsubscribeUrl: fmt.Sprintf("%s/users/unsubscribe?code=%s", siteInfo.SiteUrl, raw.UnsubscribeCode), UnsubscribeUrl: fmt.Sprintf("%s/users/unsubscribe?code=%s", siteInfo.SiteUrl, raw.UnsubscribeCode),
} }
templateData.SiteName = siteInfo.Name
lang := handler.GetLangByCtx(ctx) lang := handler.GetLangByCtx(ctx)
title = translator.TrWithData(lang, constant.EmailTplKeyNewAnswerTitle, templateData) 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 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 // NewCommentTemplate new comment template
func (es *EmailService) NewCommentTemplate(ctx context.Context, raw *schema.NewCommentTemplateRawData) ( func (es *EmailService) NewCommentTemplate(ctx context.Context, raw *schema.NewCommentTemplateRawData) (
title, body string, err error) { 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, templateData.CommentUrl = fmt.Sprintf("%s/questions/%s?commentId=%s", siteInfo.SiteUrl,
raw.QuestionID, raw.CommentID) raw.QuestionID, raw.CommentID)
} }
templateData.SiteName = siteInfo.Name
lang := handler.GetLangByCtx(ctx) lang := handler.GetLangByCtx(ctx)
title = translator.TrWithData(lang, constant.EmailTplKeyNewCommentTitle, templateData) title = translator.TrWithData(lang, constant.EmailTplKeyNewCommentTitle, templateData)

View File

@ -123,7 +123,12 @@ func (ns *NotificationService) GetNotificationPage(ctx context.Context, searchCo
if !ok { if !ok {
return pager.NewPageModel(0, resp), nil return pager.NewPageModel(0, resp), nil
} }
searchInboxType, ok := schema.NotificationInboxType[searchCond.InboxTypeStr]
if !ok {
return pager.NewPageModel(0, resp), nil
}
searchCond.Type = searchType searchCond.Type = searchType
searchCond.InboxType = searchInboxType
notifications, total, err := ns.notificationRepo.GetNotificationPage(ctx, searchCond) notifications, total, err := ns.notificationRepo.GetNotificationPage(ctx, searchCond)
if err != nil { if err != nil {
return nil, err return nil, err

View File

@ -150,6 +150,34 @@ func (qs *QuestionCommon) FindInfoByID(ctx context.Context, questionIDs []string
return list, nil 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) { func (qs *QuestionCommon) Info(ctx context.Context, questionID string, loginUserID string) (showinfo *schema.QuestionInfo, err error) {
dbinfo, has, err := qs.questionRepo.GetQuestion(ctx, questionID) dbinfo, has, err := qs.questionRepo.GetQuestion(ctx, questionID)
if err != nil { if err != nil {
@ -186,9 +214,7 @@ func (qs *QuestionCommon) Info(ctx context.Context, questionID string, loginUser
operation.Level = schema.OperationLevelInfo operation.Level = schema.OperationLevelInfo
showinfo.Operation = operation showinfo.Operation = operation
} }
} }
} }
} }

View File

@ -19,6 +19,7 @@ import (
"github.com/answerdev/answer/internal/service/activity" "github.com/answerdev/answer/internal/service/activity"
"github.com/answerdev/answer/internal/service/activity_queue" "github.com/answerdev/answer/internal/service/activity_queue"
collectioncommon "github.com/answerdev/answer/internal/service/collection_common" 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/meta"
"github.com/answerdev/answer/internal/service/notice_queue" "github.com/answerdev/answer/internal/service/notice_queue"
"github.com/answerdev/answer/internal/service/permission" "github.com/answerdev/answer/internal/service/permission"
@ -26,10 +27,12 @@ import (
"github.com/answerdev/answer/internal/service/revision_common" "github.com/answerdev/answer/internal/service/revision_common"
tagcommon "github.com/answerdev/answer/internal/service/tag_common" tagcommon "github.com/answerdev/answer/internal/service/tag_common"
usercommon "github.com/answerdev/answer/internal/service/user_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/htmltext"
"github.com/answerdev/answer/pkg/uid" "github.com/answerdev/answer/pkg/uid"
"github.com/jinzhu/copier" "github.com/jinzhu/copier"
"github.com/segmentfault/pacman/errors" "github.com/segmentfault/pacman/errors"
"github.com/segmentfault/pacman/i18n"
"github.com/segmentfault/pacman/log" "github.com/segmentfault/pacman/log"
"golang.org/x/net/context" "golang.org/x/net/context"
) )
@ -42,11 +45,13 @@ type QuestionService struct {
tagCommon *tagcommon.TagCommonService tagCommon *tagcommon.TagCommonService
questioncommon *questioncommon.QuestionCommon questioncommon *questioncommon.QuestionCommon
userCommon *usercommon.UserCommon userCommon *usercommon.UserCommon
userRepo usercommon.UserRepo
revisionService *revision_common.RevisionService revisionService *revision_common.RevisionService
metaService *meta.MetaService metaService *meta.MetaService
collectionCommon *collectioncommon.CollectionCommon collectionCommon *collectioncommon.CollectionCommon
answerActivityService *activity.AnswerActivityService answerActivityService *activity.AnswerActivityService
data *data.Data data *data.Data
emailService *export.EmailService
} }
func NewQuestionService( func NewQuestionService(
@ -54,23 +59,26 @@ func NewQuestionService(
tagCommon *tagcommon.TagCommonService, tagCommon *tagcommon.TagCommonService,
questioncommon *questioncommon.QuestionCommon, questioncommon *questioncommon.QuestionCommon,
userCommon *usercommon.UserCommon, userCommon *usercommon.UserCommon,
userRepo usercommon.UserRepo,
revisionService *revision_common.RevisionService, revisionService *revision_common.RevisionService,
metaService *meta.MetaService, metaService *meta.MetaService,
collectionCommon *collectioncommon.CollectionCommon, collectionCommon *collectioncommon.CollectionCommon,
answerActivityService *activity.AnswerActivityService, answerActivityService *activity.AnswerActivityService,
data *data.Data, data *data.Data,
emailService *export.EmailService,
) *QuestionService { ) *QuestionService {
return &QuestionService{ return &QuestionService{
questionRepo: questionRepo, questionRepo: questionRepo,
tagCommon: tagCommon, tagCommon: tagCommon,
questioncommon: questioncommon, questioncommon: questioncommon,
userCommon: userCommon, userCommon: userCommon,
userRepo: userRepo,
revisionService: revisionService, revisionService: revisionService,
metaService: metaService, metaService: metaService,
collectionCommon: collectionCommon, collectionCommon: collectionCommon,
answerActivityService: answerActivityService, answerActivityService: answerActivityService,
data: data, data: data,
emailService: emailService,
} }
} }
@ -535,6 +543,115 @@ func (qs *QuestionService) UpdateQuestionCheckTags(ctx context.Context, req *sch
return nil, nil 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 // UpdateQuestion update question
func (qs *QuestionService) UpdateQuestion(ctx context.Context, req *schema.QuestionUpdate) (questionInfo any, err error) { func (qs *QuestionService) UpdateQuestion(ctx context.Context, req *schema.QuestionUpdate) (questionInfo any, err error) {
var canUpdate bool var canUpdate bool
@ -756,6 +873,10 @@ func (qs *QuestionService) GetQuestionAndAddPV(ctx context.Context, questionID,
return qs.GetQuestion(ctx, questionID, loginUserID, per) 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 { func (qs *QuestionService) ChangeTag(ctx context.Context, objectTagData *schema.TagChange) error {
return qs.tagCommon.ObjectChangeTag(ctx, objectTagData) return qs.tagCommon.ObjectChangeTag(ctx, objectTagData)
} }

View File

@ -32,8 +32,10 @@ type UserRepo interface {
GetByUserID(ctx context.Context, userID string) (userInfo *entity.User, exist bool, err error) GetByUserID(ctx context.Context, userID string) (userInfo *entity.User, exist bool, err error)
BatchGetByID(ctx context.Context, ids []string) ([]*entity.User, error) BatchGetByID(ctx context.Context, ids []string) ([]*entity.User, error)
GetByUsername(ctx context.Context, username string) (userInfo *entity.User, exist bool, err 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) GetByEmail(ctx context.Context, email string) (userInfo *entity.User, exist bool, err error)
GetUserCount(ctx context.Context) (count int64, err error) GetUserCount(ctx context.Context) (count int64, err error)
SearchUserListByName(ctx context.Context, name string) (userList []*entity.User, err error)
} }
// UserCommon user service // UserCommon user service
@ -74,6 +76,19 @@ func (us *UserCommon) GetUserBasicInfoByUserName(ctx context.Context, username s
return info, exist, nil 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 { func (us *UserCommon) UpdateAnswerCount(ctx context.Context, userID string, num int) error {
return us.userRepo.UpdateAnswerCount(ctx, userID, num) return us.userRepo.UpdateAnswerCount(ctx, userID, num)
} }

View File

@ -814,6 +814,19 @@ func (us *UserService) getUserInfoMapping(ctx context.Context, userIDs []string)
return userInfoMapping, nil 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( func (us *UserService) warpStatRankingResp(
userInfoMapping map[string]*entity.User, userInfoMapping map[string]*entity.User,
rankStat []*entity.ActivityUserRankStat, rankStat []*entity.ActivityUserRankStat,