mirror of https://gitee.com/answerdev/answer.git
Merge branch 'release/1.0.2' into 'main'
fix(tag): It is not necessary to add a query with reserved tags when searching... See merge request opensource/answer!395
This commit is contained in:
commit
012bcdc071
2
Makefile
2
Makefile
|
@ -1,6 +1,6 @@
|
|||
.PHONY: build clean ui
|
||||
|
||||
VERSION=1.0.1
|
||||
VERSION=1.0.2
|
||||
BIN=answer
|
||||
DIR_SRC=./cmd/answer
|
||||
DOCKER_CMD=docker
|
||||
|
|
|
@ -135,7 +135,7 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database,
|
|||
tagCommonService := tag_common2.NewTagCommonService(tagCommonRepo, tagRelRepo, tagRepo, revisionService, siteInfoCommonService)
|
||||
objService := object_info.NewObjService(answerRepo, questionRepo, commentCommonRepo, tagCommonRepo, tagCommonService)
|
||||
voteRepo := activity_common.NewVoteRepo(dataData, activityRepo)
|
||||
commentService := comment2.NewCommentService(commentRepo, commentCommonRepo, userCommon, objService, voteRepo)
|
||||
commentService := comment2.NewCommentService(commentRepo, commentCommonRepo, userCommon, objService, voteRepo, emailService, userRepo)
|
||||
rolePowerRelRepo := role.NewRolePowerRelRepo(dataData)
|
||||
rolePowerRelService := role2.NewRolePowerRelService(rolePowerRelRepo, userRoleRelService)
|
||||
rankService := rank2.NewRankService(userCommon, userRankRepo, objService, userRoleRelService, rolePowerRelService, configRepo)
|
||||
|
@ -166,7 +166,7 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database,
|
|||
answerActivityService := activity2.NewAnswerActivityService(answerActivityRepo, questionActivityRepo)
|
||||
questionService := service.NewQuestionService(questionRepo, tagCommonService, questionCommon, userCommon, revisionService, metaService, collectionCommon, answerActivityService, dataData)
|
||||
questionController := controller.NewQuestionController(questionService, rankService)
|
||||
answerService := service.NewAnswerService(answerRepo, questionRepo, questionCommon, userCommon, collectionCommon, userRepo, revisionService, answerActivityService, answerCommon, voteRepo)
|
||||
answerService := service.NewAnswerService(answerRepo, questionRepo, questionCommon, userCommon, collectionCommon, userRepo, revisionService, answerActivityService, answerCommon, voteRepo, emailService)
|
||||
dashboardService := dashboard.NewDashboardService(questionRepo, answerRepo, commentCommonRepo, voteRepo, userRepo, reportRepo, configRepo, siteInfoCommonService, serviceConf, dataData)
|
||||
answerController := controller.NewAnswerController(answerService, rankService, dashboardService)
|
||||
searchParser := search_parser.NewSearchParser(tagCommonService, userCommon)
|
||||
|
|
|
@ -7,3 +7,6 @@ var Config []byte
|
|||
|
||||
//go:embed path_ignore.yaml
|
||||
var PathIgnore []byte
|
||||
|
||||
//go:embed reserved-usernames.json
|
||||
var ReservedUsernames []byte
|
||||
|
|
File diff suppressed because one or more lines are too long
247
docs/docs.go
247
docs/docs.go
|
@ -16,6 +16,22 @@ const docTemplate = `{
|
|||
"host": "{{.Host}}",
|
||||
"basePath": "{{.BasePath}}",
|
||||
"paths": {
|
||||
"/": {
|
||||
"get": {
|
||||
"description": "if config file not exist try to redirect to install page",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"installation"
|
||||
],
|
||||
"summary": "if config file not exist try to redirect to install page",
|
||||
"responses": {}
|
||||
}
|
||||
},
|
||||
"/answer/admin/api/answer/page": {
|
||||
"get": {
|
||||
"security": [
|
||||
|
@ -2687,7 +2703,7 @@ const docTemplate = `{
|
|||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"api-question"
|
||||
"Question"
|
||||
],
|
||||
"summary": "UserTop",
|
||||
"parameters": [
|
||||
|
@ -2910,7 +2926,7 @@ const docTemplate = `{
|
|||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"api-question"
|
||||
"Question"
|
||||
],
|
||||
"summary": "update question",
|
||||
"parameters": [
|
||||
|
@ -2947,7 +2963,7 @@ const docTemplate = `{
|
|||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"api-question"
|
||||
"Question"
|
||||
],
|
||||
"summary": "add question",
|
||||
"parameters": [
|
||||
|
@ -2984,7 +3000,7 @@ const docTemplate = `{
|
|||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"api-question"
|
||||
"Question"
|
||||
],
|
||||
"summary": "delete question",
|
||||
"parameters": [
|
||||
|
@ -3023,7 +3039,7 @@ const docTemplate = `{
|
|||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"api-question"
|
||||
"Question"
|
||||
],
|
||||
"summary": "close question msg list",
|
||||
"responses": {
|
||||
|
@ -3043,7 +3059,7 @@ const docTemplate = `{
|
|||
"ApiKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "GetQuestion Question",
|
||||
"description": "get question details",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
|
@ -3051,9 +3067,9 @@ const docTemplate = `{
|
|||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"api-question"
|
||||
"Question"
|
||||
],
|
||||
"summary": "GetQuestion Question",
|
||||
"summary": "get question details",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
|
@ -3076,7 +3092,7 @@ const docTemplate = `{
|
|||
},
|
||||
"/answer/api/v1/question/page": {
|
||||
"get": {
|
||||
"description": "SearchQuestionList \u003cbr\u003e \"order\" Enums(newest, active,frequent,score,unanswered)",
|
||||
"description": "get questions by page",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
|
@ -3084,17 +3100,17 @@ const docTemplate = `{
|
|||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"api-question"
|
||||
"Question"
|
||||
],
|
||||
"summary": "SearchQuestionList",
|
||||
"summary": "get questions by page",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "QuestionSearch",
|
||||
"description": "QuestionPageReq",
|
||||
"name": "data",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/schema.QuestionSearch"
|
||||
"$ref": "#/definitions/schema.QuestionPageReq"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
@ -3102,7 +3118,34 @@ const docTemplate = `{
|
|||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/handler.RespBody"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/pager.PageModel"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"list": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/schema.QuestionPageResp"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3123,7 +3166,7 @@ const docTemplate = `{
|
|||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"api-question"
|
||||
"Question"
|
||||
],
|
||||
"summary": "reopen question",
|
||||
"parameters": [
|
||||
|
@ -3147,40 +3190,6 @@ const docTemplate = `{
|
|||
}
|
||||
}
|
||||
},
|
||||
"/answer/api/v1/question/search": {
|
||||
"post": {
|
||||
"description": "SearchQuestionList",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"api-question"
|
||||
],
|
||||
"summary": "SearchQuestionList",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "QuestionSearch",
|
||||
"name": "data",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/schema.QuestionSearch"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/answer/api/v1/question/similar": {
|
||||
"get": {
|
||||
"security": [
|
||||
|
@ -3196,7 +3205,7 @@ const docTemplate = `{
|
|||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"api-question"
|
||||
"Question"
|
||||
],
|
||||
"summary": "add question title like",
|
||||
"parameters": [
|
||||
|
@ -3229,7 +3238,7 @@ const docTemplate = `{
|
|||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"api-question"
|
||||
"Question"
|
||||
],
|
||||
"summary": "Search Similar Question",
|
||||
"parameters": [
|
||||
|
@ -3267,7 +3276,7 @@ const docTemplate = `{
|
|||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"api-question"
|
||||
"Question"
|
||||
],
|
||||
"summary": "Close question",
|
||||
"parameters": [
|
||||
|
@ -4231,6 +4240,29 @@ const docTemplate = `{
|
|||
}
|
||||
}
|
||||
},
|
||||
"/answer/api/v1/user/email/notification": {
|
||||
"put": {
|
||||
"description": "unsubscribe email notification",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"User"
|
||||
],
|
||||
"summary": "unsubscribe email notification",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/handler.RespBody"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/answer/api/v1/user/email/verification": {
|
||||
"post": {
|
||||
"description": "UserVerifyEmail",
|
||||
|
@ -5119,7 +5151,7 @@ const docTemplate = `{
|
|||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"api-question"
|
||||
"Question"
|
||||
],
|
||||
"summary": "UserList",
|
||||
"parameters": [
|
||||
|
@ -6593,27 +6625,112 @@ const docTemplate = `{
|
|||
}
|
||||
}
|
||||
},
|
||||
"schema.QuestionSearch": {
|
||||
"schema.QuestionPageReq": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"order": {
|
||||
"description": "Search order by",
|
||||
"type": "string"
|
||||
"orderCond": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"newest",
|
||||
"active",
|
||||
"frequent",
|
||||
"score",
|
||||
"unanswered"
|
||||
]
|
||||
},
|
||||
"page": {
|
||||
"description": "Query number of pages",
|
||||
"type": "integer"
|
||||
"type": "integer",
|
||||
"minimum": 1
|
||||
},
|
||||
"page_size": {
|
||||
"description": "Search page size",
|
||||
"type": "integer"
|
||||
"pageSize": {
|
||||
"type": "integer",
|
||||
"minimum": 1
|
||||
},
|
||||
"tag": {
|
||||
"description": "Tags []string ` + "`" + `json:\"tags\" form:\"tags\"` + "`" + ` // Search tag",
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"maxLength": 100
|
||||
},
|
||||
"username": {
|
||||
"type": "string",
|
||||
"maxLength": 100
|
||||
}
|
||||
}
|
||||
},
|
||||
"schema.QuestionPageResp": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"accepted_answer_id": {
|
||||
"description": "answer information",
|
||||
"type": "string"
|
||||
},
|
||||
"answer_count": {
|
||||
"type": "integer"
|
||||
},
|
||||
"collection_count": {
|
||||
"type": "integer"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"follow_count": {
|
||||
"type": "integer"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"last_answer_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"operated_at": {
|
||||
"description": "operator information",
|
||||
"type": "integer"
|
||||
},
|
||||
"operation_type": {
|
||||
"type": "string"
|
||||
},
|
||||
"operator": {
|
||||
"$ref": "#/definitions/schema.QuestionPageRespOperator"
|
||||
},
|
||||
"status": {
|
||||
"type": "integer"
|
||||
},
|
||||
"tags": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/schema.TagResp"
|
||||
}
|
||||
},
|
||||
"title": {
|
||||
"type": "string"
|
||||
},
|
||||
"unique_view_count": {
|
||||
"type": "integer"
|
||||
},
|
||||
"url_title": {
|
||||
"type": "string"
|
||||
},
|
||||
"view_count": {
|
||||
"description": "question statistical information",
|
||||
"type": "integer"
|
||||
},
|
||||
"vote_count": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"schema.QuestionPageRespOperator": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"display_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"rank": {
|
||||
"type": "integer"
|
||||
},
|
||||
"username": {
|
||||
"description": "Search username",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,22 @@
|
|||
"contact": {}
|
||||
},
|
||||
"paths": {
|
||||
"/": {
|
||||
"get": {
|
||||
"description": "if config file not exist try to redirect to install page",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"installation"
|
||||
],
|
||||
"summary": "if config file not exist try to redirect to install page",
|
||||
"responses": {}
|
||||
}
|
||||
},
|
||||
"/answer/admin/api/answer/page": {
|
||||
"get": {
|
||||
"security": [
|
||||
|
@ -2675,7 +2691,7 @@
|
|||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"api-question"
|
||||
"Question"
|
||||
],
|
||||
"summary": "UserTop",
|
||||
"parameters": [
|
||||
|
@ -2898,7 +2914,7 @@
|
|||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"api-question"
|
||||
"Question"
|
||||
],
|
||||
"summary": "update question",
|
||||
"parameters": [
|
||||
|
@ -2935,7 +2951,7 @@
|
|||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"api-question"
|
||||
"Question"
|
||||
],
|
||||
"summary": "add question",
|
||||
"parameters": [
|
||||
|
@ -2972,7 +2988,7 @@
|
|||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"api-question"
|
||||
"Question"
|
||||
],
|
||||
"summary": "delete question",
|
||||
"parameters": [
|
||||
|
@ -3011,7 +3027,7 @@
|
|||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"api-question"
|
||||
"Question"
|
||||
],
|
||||
"summary": "close question msg list",
|
||||
"responses": {
|
||||
|
@ -3031,7 +3047,7 @@
|
|||
"ApiKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "GetQuestion Question",
|
||||
"description": "get question details",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
|
@ -3039,9 +3055,9 @@
|
|||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"api-question"
|
||||
"Question"
|
||||
],
|
||||
"summary": "GetQuestion Question",
|
||||
"summary": "get question details",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
|
@ -3064,7 +3080,7 @@
|
|||
},
|
||||
"/answer/api/v1/question/page": {
|
||||
"get": {
|
||||
"description": "SearchQuestionList \u003cbr\u003e \"order\" Enums(newest, active,frequent,score,unanswered)",
|
||||
"description": "get questions by page",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
|
@ -3072,17 +3088,17 @@
|
|||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"api-question"
|
||||
"Question"
|
||||
],
|
||||
"summary": "SearchQuestionList",
|
||||
"summary": "get questions by page",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "QuestionSearch",
|
||||
"description": "QuestionPageReq",
|
||||
"name": "data",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/schema.QuestionSearch"
|
||||
"$ref": "#/definitions/schema.QuestionPageReq"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
@ -3090,7 +3106,34 @@
|
|||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/handler.RespBody"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/pager.PageModel"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"list": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/schema.QuestionPageResp"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3111,7 +3154,7 @@
|
|||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"api-question"
|
||||
"Question"
|
||||
],
|
||||
"summary": "reopen question",
|
||||
"parameters": [
|
||||
|
@ -3135,40 +3178,6 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"/answer/api/v1/question/search": {
|
||||
"post": {
|
||||
"description": "SearchQuestionList",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"api-question"
|
||||
],
|
||||
"summary": "SearchQuestionList",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "QuestionSearch",
|
||||
"name": "data",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/schema.QuestionSearch"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/answer/api/v1/question/similar": {
|
||||
"get": {
|
||||
"security": [
|
||||
|
@ -3184,7 +3193,7 @@
|
|||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"api-question"
|
||||
"Question"
|
||||
],
|
||||
"summary": "add question title like",
|
||||
"parameters": [
|
||||
|
@ -3217,7 +3226,7 @@
|
|||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"api-question"
|
||||
"Question"
|
||||
],
|
||||
"summary": "Search Similar Question",
|
||||
"parameters": [
|
||||
|
@ -3255,7 +3264,7 @@
|
|||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"api-question"
|
||||
"Question"
|
||||
],
|
||||
"summary": "Close question",
|
||||
"parameters": [
|
||||
|
@ -4219,6 +4228,29 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"/answer/api/v1/user/email/notification": {
|
||||
"put": {
|
||||
"description": "unsubscribe email notification",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"User"
|
||||
],
|
||||
"summary": "unsubscribe email notification",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/handler.RespBody"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/answer/api/v1/user/email/verification": {
|
||||
"post": {
|
||||
"description": "UserVerifyEmail",
|
||||
|
@ -5107,7 +5139,7 @@
|
|||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"api-question"
|
||||
"Question"
|
||||
],
|
||||
"summary": "UserList",
|
||||
"parameters": [
|
||||
|
@ -6581,27 +6613,112 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"schema.QuestionSearch": {
|
||||
"schema.QuestionPageReq": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"order": {
|
||||
"description": "Search order by",
|
||||
"type": "string"
|
||||
"orderCond": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"newest",
|
||||
"active",
|
||||
"frequent",
|
||||
"score",
|
||||
"unanswered"
|
||||
]
|
||||
},
|
||||
"page": {
|
||||
"description": "Query number of pages",
|
||||
"type": "integer"
|
||||
"type": "integer",
|
||||
"minimum": 1
|
||||
},
|
||||
"page_size": {
|
||||
"description": "Search page size",
|
||||
"type": "integer"
|
||||
"pageSize": {
|
||||
"type": "integer",
|
||||
"minimum": 1
|
||||
},
|
||||
"tag": {
|
||||
"description": "Tags []string `json:\"tags\" form:\"tags\"` // Search tag",
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"maxLength": 100
|
||||
},
|
||||
"username": {
|
||||
"type": "string",
|
||||
"maxLength": 100
|
||||
}
|
||||
}
|
||||
},
|
||||
"schema.QuestionPageResp": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"accepted_answer_id": {
|
||||
"description": "answer information",
|
||||
"type": "string"
|
||||
},
|
||||
"answer_count": {
|
||||
"type": "integer"
|
||||
},
|
||||
"collection_count": {
|
||||
"type": "integer"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"follow_count": {
|
||||
"type": "integer"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"last_answer_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"operated_at": {
|
||||
"description": "operator information",
|
||||
"type": "integer"
|
||||
},
|
||||
"operation_type": {
|
||||
"type": "string"
|
||||
},
|
||||
"operator": {
|
||||
"$ref": "#/definitions/schema.QuestionPageRespOperator"
|
||||
},
|
||||
"status": {
|
||||
"type": "integer"
|
||||
},
|
||||
"tags": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/schema.TagResp"
|
||||
}
|
||||
},
|
||||
"title": {
|
||||
"type": "string"
|
||||
},
|
||||
"unique_view_count": {
|
||||
"type": "integer"
|
||||
},
|
||||
"url_title": {
|
||||
"type": "string"
|
||||
},
|
||||
"view_count": {
|
||||
"description": "question statistical information",
|
||||
"type": "integer"
|
||||
},
|
||||
"vote_count": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"schema.QuestionPageRespOperator": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"display_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"rank": {
|
||||
"type": "integer"
|
||||
},
|
||||
"username": {
|
||||
"description": "Search username",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1004,23 +1004,80 @@ definitions:
|
|||
- tags
|
||||
- title
|
||||
type: object
|
||||
schema.QuestionSearch:
|
||||
schema.QuestionPageReq:
|
||||
properties:
|
||||
order:
|
||||
description: Search order by
|
||||
orderCond:
|
||||
enum:
|
||||
- newest
|
||||
- active
|
||||
- frequent
|
||||
- score
|
||||
- unanswered
|
||||
type: string
|
||||
page:
|
||||
description: Query number of pages
|
||||
minimum: 1
|
||||
type: integer
|
||||
page_size:
|
||||
description: Search page size
|
||||
pageSize:
|
||||
minimum: 1
|
||||
type: integer
|
||||
tag:
|
||||
description: Tags []string `json:"tags" form:"tags"` // Search
|
||||
tag
|
||||
maxLength: 100
|
||||
type: string
|
||||
username:
|
||||
description: Search username
|
||||
maxLength: 100
|
||||
type: string
|
||||
type: object
|
||||
schema.QuestionPageResp:
|
||||
properties:
|
||||
accepted_answer_id:
|
||||
description: answer information
|
||||
type: string
|
||||
answer_count:
|
||||
type: integer
|
||||
collection_count:
|
||||
type: integer
|
||||
description:
|
||||
type: string
|
||||
follow_count:
|
||||
type: integer
|
||||
id:
|
||||
type: string
|
||||
last_answer_id:
|
||||
type: string
|
||||
operated_at:
|
||||
description: operator information
|
||||
type: integer
|
||||
operation_type:
|
||||
type: string
|
||||
operator:
|
||||
$ref: '#/definitions/schema.QuestionPageRespOperator'
|
||||
status:
|
||||
type: integer
|
||||
tags:
|
||||
items:
|
||||
$ref: '#/definitions/schema.TagResp'
|
||||
type: array
|
||||
title:
|
||||
type: string
|
||||
unique_view_count:
|
||||
type: integer
|
||||
url_title:
|
||||
type: string
|
||||
view_count:
|
||||
description: question statistical information
|
||||
type: integer
|
||||
vote_count:
|
||||
type: integer
|
||||
type: object
|
||||
schema.QuestionPageRespOperator:
|
||||
properties:
|
||||
display_name:
|
||||
type: string
|
||||
id:
|
||||
type: string
|
||||
rank:
|
||||
type: integer
|
||||
username:
|
||||
type: string
|
||||
type: object
|
||||
schema.QuestionUpdate:
|
||||
|
@ -1886,6 +1943,17 @@ definitions:
|
|||
info:
|
||||
contact: {}
|
||||
paths:
|
||||
/:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: if config file not exist try to redirect to install page
|
||||
produces:
|
||||
- application/json
|
||||
responses: {}
|
||||
summary: if config file not exist try to redirect to install page
|
||||
tags:
|
||||
- installation
|
||||
/answer/admin/api/answer/page:
|
||||
get:
|
||||
consumes:
|
||||
|
@ -3517,7 +3585,7 @@ paths:
|
|||
- ApiKeyAuth: []
|
||||
summary: UserTop
|
||||
tags:
|
||||
- api-question
|
||||
- Question
|
||||
/answer/api/v1/personal/rank/page:
|
||||
get:
|
||||
description: user personal rank list
|
||||
|
@ -3645,7 +3713,7 @@ paths:
|
|||
- ApiKeyAuth: []
|
||||
summary: delete question
|
||||
tags:
|
||||
- api-question
|
||||
- Question
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
|
@ -3668,7 +3736,7 @@ paths:
|
|||
- ApiKeyAuth: []
|
||||
summary: add question
|
||||
tags:
|
||||
- api-question
|
||||
- Question
|
||||
put:
|
||||
consumes:
|
||||
- application/json
|
||||
|
@ -3691,7 +3759,7 @@ paths:
|
|||
- ApiKeyAuth: []
|
||||
summary: update question
|
||||
tags:
|
||||
- api-question
|
||||
- Question
|
||||
/answer/api/v1/question/closemsglist:
|
||||
get:
|
||||
consumes:
|
||||
|
@ -3708,12 +3776,12 @@ paths:
|
|||
- ApiKeyAuth: []
|
||||
summary: close question msg list
|
||||
tags:
|
||||
- api-question
|
||||
- Question
|
||||
/answer/api/v1/question/info:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: GetQuestion Question
|
||||
description: get question details
|
||||
parameters:
|
||||
- default: "1"
|
||||
description: Question TagID
|
||||
|
@ -3730,31 +3798,43 @@ paths:
|
|||
type: string
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
summary: GetQuestion Question
|
||||
summary: get question details
|
||||
tags:
|
||||
- api-question
|
||||
- Question
|
||||
/answer/api/v1/question/page:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: SearchQuestionList <br> "order" Enums(newest, active,frequent,score,unanswered)
|
||||
description: get questions by page
|
||||
parameters:
|
||||
- description: QuestionSearch
|
||||
- description: QuestionPageReq
|
||||
in: body
|
||||
name: data
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/schema.QuestionSearch'
|
||||
$ref: '#/definitions/schema.QuestionPageReq'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
type: string
|
||||
summary: SearchQuestionList
|
||||
allOf:
|
||||
- $ref: '#/definitions/handler.RespBody'
|
||||
- properties:
|
||||
data:
|
||||
allOf:
|
||||
- $ref: '#/definitions/pager.PageModel'
|
||||
- properties:
|
||||
list:
|
||||
items:
|
||||
$ref: '#/definitions/schema.QuestionPageResp'
|
||||
type: array
|
||||
type: object
|
||||
type: object
|
||||
summary: get questions by page
|
||||
tags:
|
||||
- api-question
|
||||
- Question
|
||||
/answer/api/v1/question/reopen:
|
||||
put:
|
||||
consumes:
|
||||
|
@ -3778,29 +3858,7 @@ paths:
|
|||
- ApiKeyAuth: []
|
||||
summary: reopen question
|
||||
tags:
|
||||
- api-question
|
||||
/answer/api/v1/question/search:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: SearchQuestionList
|
||||
parameters:
|
||||
- description: QuestionSearch
|
||||
in: body
|
||||
name: data
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/schema.QuestionSearch'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
type: string
|
||||
summary: SearchQuestionList
|
||||
tags:
|
||||
- api-question
|
||||
- Question
|
||||
/answer/api/v1/question/similar:
|
||||
get:
|
||||
consumes:
|
||||
|
@ -3824,7 +3882,7 @@ paths:
|
|||
- ApiKeyAuth: []
|
||||
summary: add question title like
|
||||
tags:
|
||||
- api-question
|
||||
- Question
|
||||
/answer/api/v1/question/similar/tag:
|
||||
get:
|
||||
consumes:
|
||||
|
@ -3846,7 +3904,7 @@ paths:
|
|||
type: string
|
||||
summary: Search Similar Question
|
||||
tags:
|
||||
- api-question
|
||||
- Question
|
||||
/answer/api/v1/question/status:
|
||||
put:
|
||||
consumes:
|
||||
|
@ -3870,7 +3928,7 @@ paths:
|
|||
- ApiKeyAuth: []
|
||||
summary: Close question
|
||||
tags:
|
||||
- api-question
|
||||
- Question
|
||||
/answer/api/v1/question/tags:
|
||||
get:
|
||||
description: get tag list
|
||||
|
@ -4439,6 +4497,21 @@ paths:
|
|||
summary: send email to the user email then change their email
|
||||
tags:
|
||||
- User
|
||||
/answer/api/v1/user/email/notification:
|
||||
put:
|
||||
consumes:
|
||||
- application/json
|
||||
description: unsubscribe email notification
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/handler.RespBody'
|
||||
summary: unsubscribe email notification
|
||||
tags:
|
||||
- User
|
||||
/answer/api/v1/user/email/verification:
|
||||
post:
|
||||
consumes:
|
||||
|
@ -5007,7 +5080,7 @@ paths:
|
|||
- ApiKeyAuth: []
|
||||
summary: UserList
|
||||
tags:
|
||||
- api-question
|
||||
- Question
|
||||
/robots.txt:
|
||||
get:
|
||||
description: get site robots information
|
||||
|
|
|
@ -111,6 +111,9 @@ backend:
|
|||
other: "No permission to update."
|
||||
cannot_set_synonym_as_itself:
|
||||
other: "You cannot set the synonym of the current tag as itself."
|
||||
smtp:
|
||||
config_from_name_cannot_be_email:
|
||||
other: "The From Name cannot be a email address."
|
||||
theme:
|
||||
not_found:
|
||||
other: "Theme not found."
|
||||
|
@ -132,21 +135,21 @@ backend:
|
|||
other: "Username is already in use."
|
||||
set_avatar:
|
||||
other: "Avatar set failed."
|
||||
config:
|
||||
read_config_failed:
|
||||
other: "Read config failed"
|
||||
database:
|
||||
connection_failed:
|
||||
other: "Database connection failed"
|
||||
create_table_failed:
|
||||
other: "Create table failed"
|
||||
install:
|
||||
create_config_failed:
|
||||
other: "Can’t create the config.yaml file."
|
||||
cannot_update_your_role:
|
||||
other: "You cannot modify your role."
|
||||
not_allowed_registration:
|
||||
other: "Currently the site is not open for registration"
|
||||
config:
|
||||
read_config_failed:
|
||||
other: "Read config failed"
|
||||
database:
|
||||
connection_failed:
|
||||
other: "Database connection failed"
|
||||
create_table_failed:
|
||||
other: "Create table failed"
|
||||
install:
|
||||
create_config_failed:
|
||||
other: "Can’t create the config.yaml file."
|
||||
report:
|
||||
spam:
|
||||
name:
|
||||
|
@ -201,7 +204,13 @@ backend:
|
|||
other: "something else"
|
||||
desc:
|
||||
other: "This post requires another reason not listed above."
|
||||
|
||||
operation_type:
|
||||
asked:
|
||||
other: "asked"
|
||||
answered:
|
||||
other: "answered"
|
||||
modified:
|
||||
other: "modified"
|
||||
notification:
|
||||
action:
|
||||
update_question:
|
||||
|
@ -838,6 +847,11 @@ ui:
|
|||
confirm_new_email_invalid: >-
|
||||
Sorry, this confirmation link is no longer valid. Perhaps your email was
|
||||
already changed?
|
||||
unsubscribe:
|
||||
page_title: Unsubscribe
|
||||
success_title: Unsubscribe Successful
|
||||
success_desc: You have been successfully removed from this subscriber list and won’t receive any further emails from us.
|
||||
link: Change settings
|
||||
question:
|
||||
following_tags: Following Tags
|
||||
edit: Edit
|
||||
|
@ -1034,6 +1048,7 @@ ui:
|
|||
answer_links: Answer Links
|
||||
documents: Documents
|
||||
feedback: Feedback
|
||||
support: Support
|
||||
review: Review
|
||||
config: Config
|
||||
update_to: Update to
|
||||
|
|
|
@ -4,10 +4,35 @@ import (
|
|||
"github.com/answerdev/answer/internal/base/constant"
|
||||
"github.com/answerdev/answer/internal/base/handler"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/segmentfault/pacman/i18n"
|
||||
)
|
||||
|
||||
var (
|
||||
langMapping = map[i18n.Language]bool{
|
||||
i18n.LanguageChinese: true,
|
||||
i18n.LanguageChineseTraditional: true,
|
||||
i18n.LanguageEnglish: true,
|
||||
i18n.LanguageGerman: true,
|
||||
i18n.LanguageSpanish: true,
|
||||
i18n.LanguageFrench: true,
|
||||
i18n.LanguageItalian: true,
|
||||
i18n.LanguageJapanese: true,
|
||||
i18n.LanguageKorean: true,
|
||||
i18n.LanguagePortuguese: true,
|
||||
i18n.LanguageRussian: true,
|
||||
i18n.LanguageVietnamese: true,
|
||||
}
|
||||
)
|
||||
|
||||
// ExtractAndSetAcceptLanguage extract accept language from header and set to context
|
||||
func ExtractAndSetAcceptLanguage(ctx *gin.Context) {
|
||||
// The language of our front-end configuration, like en_US
|
||||
lang := handler.GetLang(ctx)
|
||||
ctx.Set(constant.AcceptLanguageFlag, lang)
|
||||
if langMapping[lang] {
|
||||
ctx.Set(constant.AcceptLanguageFlag, lang)
|
||||
return
|
||||
}
|
||||
|
||||
// default language
|
||||
ctx.Set(constant.AcceptLanguageFlag, i18n.LanguageEnglish)
|
||||
}
|
||||
|
|
|
@ -60,4 +60,5 @@ const (
|
|||
UserCannotUpdateYourRole = "error.user.cannot_update_your_role"
|
||||
TagCannotSetSynonymAsItself = "error.tag.cannot_set_synonym_as_itself"
|
||||
NotAllowedRegistration = "error.user.not_allowed_registration"
|
||||
SMTPConfigFromNameCannotBeEmail = "error.smtp.config_from_name_cannot_be_email"
|
||||
)
|
||||
|
|
|
@ -12,8 +12,9 @@ import (
|
|||
)
|
||||
|
||||
const (
|
||||
DefaultConfigFileName = "config.yaml"
|
||||
DefaultCacheFileName = "cache.db"
|
||||
DefaultConfigFileName = "config.yaml"
|
||||
DefaultCacheFileName = "cache.db"
|
||||
DefaultReservedUsernamesConfigFileName = "reserved-usernames.json"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -40,6 +41,7 @@ func InstallAllInitialEnvironment(dataDirPath string) {
|
|||
FormatAllPath(dataDirPath)
|
||||
installUploadDir()
|
||||
installI18nBundle()
|
||||
installReservedUsernames()
|
||||
fmt.Println("install all initial environment done")
|
||||
}
|
||||
|
||||
|
@ -112,3 +114,16 @@ func installI18nBundle() {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
func installReservedUsernames() {
|
||||
reservedUsernamesJsonFilePath := filepath.Join(ConfigFileDir, DefaultReservedUsernamesConfigFileName)
|
||||
if !dir.CheckFileExist(reservedUsernamesJsonFilePath) {
|
||||
err := writer.WriteFile(reservedUsernamesJsonFilePath, string(configs.ReservedUsernames))
|
||||
if err != nil {
|
||||
fmt.Printf("[%s] write file fail: %s\n", DefaultReservedUsernamesConfigFileName, err)
|
||||
} else {
|
||||
fmt.Printf("[%s] write file success\n", DefaultReservedUsernamesConfigFileName)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/answerdev/answer/internal/base/handler"
|
||||
"github.com/answerdev/answer/internal/base/middleware"
|
||||
"github.com/answerdev/answer/internal/base/pager"
|
||||
"github.com/answerdev/answer/internal/base/reason"
|
||||
"github.com/answerdev/answer/internal/base/validator"
|
||||
"github.com/answerdev/answer/internal/entity"
|
||||
|
@ -31,7 +30,7 @@ func NewQuestionController(questionService *service.QuestionService, rankService
|
|||
// RemoveQuestion delete question
|
||||
// @Summary delete question
|
||||
// @Description delete question
|
||||
// @Tags api-question
|
||||
// @Tags Question
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
|
@ -62,7 +61,7 @@ func (qc *QuestionController) RemoveQuestion(ctx *gin.Context) {
|
|||
// CloseQuestion Close question
|
||||
// @Summary Close question
|
||||
// @Description Close question
|
||||
// @Tags api-question
|
||||
// @Tags Question
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
|
@ -92,7 +91,7 @@ func (qc *QuestionController) CloseQuestion(ctx *gin.Context) {
|
|||
// ReopenQuestion reopen question
|
||||
// @Summary reopen question
|
||||
// @Description reopen question
|
||||
// @Tags api-question
|
||||
// @Tags Question
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
|
@ -119,10 +118,10 @@ func (qc *QuestionController) ReopenQuestion(ctx *gin.Context) {
|
|||
handler.HandleResponse(ctx, err, nil)
|
||||
}
|
||||
|
||||
// GetQuestion godoc
|
||||
// @Summary GetQuestion Question
|
||||
// @Description GetQuestion Question
|
||||
// @Tags api-question
|
||||
// GetQuestion get question details
|
||||
// @Summary get question details
|
||||
// @Description get question details
|
||||
// @Tags Question
|
||||
// @Security ApiKeyAuth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
|
@ -161,7 +160,7 @@ func (qc *QuestionController) GetQuestion(ctx *gin.Context) {
|
|||
// SimilarQuestion godoc
|
||||
// @Summary Search Similar Question
|
||||
// @Description Search Similar Question
|
||||
// @Tags api-question
|
||||
// @Tags Question
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param question_id query string true "question_id" default()
|
||||
|
@ -181,65 +180,34 @@ func (qc *QuestionController) SimilarQuestion(ctx *gin.Context) {
|
|||
})
|
||||
}
|
||||
|
||||
// Index godoc
|
||||
// @Summary SearchQuestionList
|
||||
// @Description SearchQuestionList <br> "order" Enums(newest, active,frequent,score,unanswered)
|
||||
// @Tags api-question
|
||||
// QuestionPage get questions by page
|
||||
// @Summary get questions by page
|
||||
// @Description get questions by page
|
||||
// @Tags Question
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param data body schema.QuestionSearch true "QuestionSearch"
|
||||
// @Success 200 {string} string ""
|
||||
// @Param data body schema.QuestionPageReq true "QuestionPageReq"
|
||||
// @Success 200 {object} handler.RespBody{data=pager.PageModel{list=[]schema.QuestionPageResp}}
|
||||
// @Router /answer/api/v1/question/page [get]
|
||||
func (qc *QuestionController) Index(ctx *gin.Context) {
|
||||
req := &schema.QuestionSearch{}
|
||||
func (qc *QuestionController) QuestionPage(ctx *gin.Context) {
|
||||
req := &schema.QuestionPageReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
userID := middleware.GetLoginUserIDFromContext(ctx)
|
||||
list, count, err := qc.questionService.SearchList(ctx, req, userID)
|
||||
req.LoginUserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||
|
||||
questions, total, err := qc.questionService.GetQuestionPage(ctx, req)
|
||||
if err != nil {
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
return
|
||||
}
|
||||
handler.HandleResponse(ctx, nil, gin.H{
|
||||
"list": list,
|
||||
"count": count,
|
||||
})
|
||||
}
|
||||
|
||||
// SearchList godoc
|
||||
// @Summary SearchQuestionList
|
||||
// @Description SearchQuestionList
|
||||
// @Tags api-question
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param data body schema.QuestionSearch true "QuestionSearch"
|
||||
// @Router /answer/api/v1/question/search [post]
|
||||
// @Success 200 {string} string ""
|
||||
func (qc *QuestionController) SearchList(c *gin.Context) {
|
||||
Request := new(schema.QuestionSearch)
|
||||
err := c.BindJSON(Request)
|
||||
if err != nil {
|
||||
handler.HandleResponse(c, err, nil)
|
||||
return
|
||||
}
|
||||
ctx := context.Background()
|
||||
userID := middleware.GetLoginUserIDFromContext(c)
|
||||
list, count, err := qc.questionService.SearchList(ctx, Request, userID)
|
||||
if err != nil {
|
||||
handler.HandleResponse(c, err, nil)
|
||||
return
|
||||
}
|
||||
handler.HandleResponse(c, nil, gin.H{
|
||||
"list": list,
|
||||
"count": count,
|
||||
})
|
||||
handler.HandleResponse(ctx, nil, pager.NewPageModel(total, questions))
|
||||
}
|
||||
|
||||
// AddQuestion add question
|
||||
// @Summary add question
|
||||
// @Description add question
|
||||
// @Tags api-question
|
||||
// @Tags Question
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
|
@ -309,7 +277,7 @@ func (qc *QuestionController) AddQuestion(ctx *gin.Context) {
|
|||
// UpdateQuestion update question
|
||||
// @Summary update question
|
||||
// @Description update question
|
||||
// @Tags api-question
|
||||
// @Tags Question
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
|
@ -366,7 +334,7 @@ func (qc *QuestionController) UpdateQuestion(ctx *gin.Context) {
|
|||
// CloseMsgList close question msg list
|
||||
// @Summary close question msg list
|
||||
// @Description close question msg list
|
||||
// @Tags api-question
|
||||
// @Tags Question
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
|
@ -380,7 +348,7 @@ func (qc *QuestionController) CloseMsgList(ctx *gin.Context) {
|
|||
// SearchByTitleLike add question title like
|
||||
// @Summary add question title like
|
||||
// @Description add question title like
|
||||
// @Tags api-question
|
||||
// @Tags Question
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
|
@ -397,7 +365,7 @@ func (qc *QuestionController) SearchByTitleLike(ctx *gin.Context) {
|
|||
// UserTop godoc
|
||||
// @Summary UserTop
|
||||
// @Description UserTop
|
||||
// @Tags api-question
|
||||
// @Tags Question
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
|
@ -417,7 +385,7 @@ func (qc *QuestionController) UserTop(ctx *gin.Context) {
|
|||
// UserList godoc
|
||||
// @Summary UserList
|
||||
// @Description UserList
|
||||
// @Tags api-question
|
||||
// @Tags Question
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
|
|
|
@ -91,8 +91,8 @@ func (tc *TemplateController) SiteInfo(ctx *gin.Context) *schema.TemplateSiteInf
|
|||
|
||||
// Index question list
|
||||
func (tc *TemplateController) Index(ctx *gin.Context) {
|
||||
req := &schema.QuestionSearch{
|
||||
Order: "newest",
|
||||
req := &schema.QuestionPageReq{
|
||||
OrderCond: "newest",
|
||||
}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
tc.Page404(ctx)
|
||||
|
@ -124,8 +124,8 @@ func (tc *TemplateController) Index(ctx *gin.Context) {
|
|||
}
|
||||
|
||||
func (tc *TemplateController) QuestionList(ctx *gin.Context) {
|
||||
req := &schema.QuestionSearch{
|
||||
Order: "newest",
|
||||
req := &schema.QuestionPageReq{
|
||||
OrderCond: "newest",
|
||||
}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
tc.Page404(ctx)
|
||||
|
@ -152,7 +152,7 @@ func (tc *TemplateController) QuestionList(ctx *gin.Context) {
|
|||
})
|
||||
}
|
||||
|
||||
func (tc *TemplateController) QuestionInfo301Jump(ctx *gin.Context, siteInfo *schema.TemplateSiteInfoResp, correctTitle bool) (jump bool, url string) {
|
||||
func (tc *TemplateController) QuestionInfoeRdirect(ctx *gin.Context, siteInfo *schema.TemplateSiteInfoResp, correctTitle bool) (jump bool, url string) {
|
||||
id := ctx.Param("id")
|
||||
title := ctx.Param("title")
|
||||
titleIsAnswerID := false
|
||||
|
@ -182,6 +182,9 @@ func (tc *TemplateController) QuestionInfo301Jump(ctx *gin.Context, siteInfo *sc
|
|||
return
|
||||
}
|
||||
url = fmt.Sprintf("%s/%s", url, htmltext.UrlTitle(detail.Title))
|
||||
if titleIsAnswerID {
|
||||
url = fmt.Sprintf("%s/%s", url, title)
|
||||
}
|
||||
return true, url
|
||||
}
|
||||
}
|
||||
|
@ -217,9 +220,9 @@ func (tc *TemplateController) QuestionInfo(ctx *gin.Context) {
|
|||
}
|
||||
|
||||
siteInfo := tc.SiteInfo(ctx)
|
||||
jump, jumpurl := tc.QuestionInfo301Jump(ctx, siteInfo, correctTitle)
|
||||
jump, jumpurl := tc.QuestionInfoeRdirect(ctx, siteInfo, correctTitle)
|
||||
if jump {
|
||||
ctx.Redirect(http.StatusMovedPermanently, jumpurl)
|
||||
ctx.Redirect(http.StatusFound, jumpurl)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -11,8 +11,8 @@ import (
|
|||
"github.com/segmentfault/pacman/log"
|
||||
)
|
||||
|
||||
func (t *TemplateRenderController) Index(ctx *gin.Context, req *schema.QuestionSearch) ([]*schema.QuestionInfo, int64, error) {
|
||||
return t.questionService.SearchList(ctx, req, req.UserID)
|
||||
func (t *TemplateRenderController) Index(ctx *gin.Context, req *schema.QuestionPageReq) ([]*schema.QuestionPageResp, int64, error) {
|
||||
return t.questionService.GetQuestionPage(ctx, req)
|
||||
}
|
||||
|
||||
func (t *TemplateRenderController) QuestionDetail(ctx *gin.Context, id string) (resp *schema.QuestionInfo, err error) {
|
||||
|
|
|
@ -15,19 +15,20 @@ func (q *TemplateRenderController) TagList(ctx context.Context, req *schema.GetT
|
|||
return
|
||||
}
|
||||
|
||||
func (q *TemplateRenderController) TagInfo(ctx context.Context, req *schema.GetTamplateTagInfoReq) (resp *schema.GetTagResp, questionList []*schema.QuestionInfo, questionCount int64, err error) {
|
||||
func (q *TemplateRenderController) TagInfo(ctx context.Context, req *schema.GetTamplateTagInfoReq) (resp *schema.GetTagResp, questionList []*schema.QuestionPageResp, questionCount int64, err error) {
|
||||
dto := &schema.GetTagInfoReq{}
|
||||
_ = copier.Copy(dto, req)
|
||||
resp, err = q.tagService.GetTagInfo(ctx, dto)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
searchQuestion := &schema.QuestionSearch{}
|
||||
searchQuestion := &schema.QuestionPageReq{}
|
||||
searchQuestion.Page = req.Page
|
||||
searchQuestion.PageSize = req.PageSize
|
||||
searchQuestion.Order = "newest"
|
||||
searchQuestion.OrderCond = "newest"
|
||||
searchQuestion.Tag = req.Name
|
||||
questionList, questionCount, err = q.questionService.SearchList(ctx, searchQuestion, "")
|
||||
searchQuestion.LoginUserID = req.UserID
|
||||
questionList, questionCount, err = q.questionService.GetQuestionPage(ctx, searchQuestion)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
|
|
@ -242,8 +242,15 @@ func (uc *UserController) UserRegisterByEmail(ctx *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
resp, err := uc.userService.UserRegisterByEmail(ctx, req)
|
||||
handler.HandleResponse(ctx, err, resp)
|
||||
resp, errFields, err := uc.userService.UserRegisterByEmail(ctx, req)
|
||||
if len(errFields) > 0 {
|
||||
for _, field := range errFields {
|
||||
field.ErrorMsg = translator.GlobalTrans.Tr(handler.GetLang(ctx), field.ErrorMsg)
|
||||
}
|
||||
handler.HandleResponse(ctx, err, errFields)
|
||||
} else {
|
||||
handler.HandleResponse(ctx, err, resp)
|
||||
}
|
||||
}
|
||||
|
||||
// UserVerifyEmail godoc
|
||||
|
@ -377,8 +384,11 @@ func (uc *UserController) UserUpdateInfo(ctx *gin.Context) {
|
|||
return
|
||||
}
|
||||
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||
err := uc.userService.UpdateInfo(ctx, req)
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
errFields, err := uc.userService.UpdateInfo(ctx, req)
|
||||
for _, field := range errFields {
|
||||
field.ErrorMsg = translator.GlobalTrans.Tr(handler.GetLang(ctx), field.ErrorMsg)
|
||||
}
|
||||
handler.HandleResponse(ctx, err, errFields)
|
||||
}
|
||||
|
||||
// UserUpdateInterface update user interface config
|
||||
|
@ -535,3 +545,28 @@ func (uc *UserController) UserRanking(ctx *gin.Context) {
|
|||
resp, err := uc.userService.UserRanking(ctx)
|
||||
handler.HandleResponse(ctx, err, resp)
|
||||
}
|
||||
|
||||
// UserUnsubscribeEmailNotification unsubscribe email notification
|
||||
// @Summary unsubscribe email notification
|
||||
// @Description unsubscribe email notification
|
||||
// @Tags User
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} handler.RespBody{}
|
||||
// @Router /answer/api/v1/user/email/notification [put]
|
||||
func (uc *UserController) UserUnsubscribeEmailNotification(ctx *gin.Context) {
|
||||
req := &schema.UserUnsubscribeEmailNotificationReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
|
||||
req.Content = uc.emailService.VerifyUrlExpired(ctx, req.Code)
|
||||
if len(req.Content) == 0 {
|
||||
handler.HandleResponse(ctx, errors.Forbidden(reason.EmailVerifyURLExpired),
|
||||
&schema.ForbiddenResp{Type: schema.ForbiddenReasonTypeURLExpired})
|
||||
return
|
||||
}
|
||||
|
||||
err := uc.userService.UserUnsubscribeEmailNotification(ctx, req)
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
}
|
||||
|
|
|
@ -22,11 +22,6 @@ var AdminQuestionSearchStatusIntToString = map[int]string{
|
|||
QuestionStatusDeleted: "deleted",
|
||||
}
|
||||
|
||||
type QuestionTag struct {
|
||||
Question `xorm:"extends"`
|
||||
TagRel `xorm:"extends"`
|
||||
}
|
||||
|
||||
// Question question
|
||||
type Question struct {
|
||||
ID string `xorm:"not null pk BIGINT(20) id"`
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package install
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
@ -30,6 +31,21 @@ func LangOptions(ctx *gin.Context) {
|
|||
handler.HandleResponse(ctx, nil, translator.LanguageOptions)
|
||||
}
|
||||
|
||||
// CheckConfigFileAndRedirectToInstallPage if config file not exist try to redirect to install page
|
||||
// @Summary if config file not exist try to redirect to install page
|
||||
// @Description if config file not exist try to redirect to install page
|
||||
// @Tags installation
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Router / [get]
|
||||
func CheckConfigFileAndRedirectToInstallPage(ctx *gin.Context) {
|
||||
if cli.CheckConfigFile(confPath) {
|
||||
ctx.Redirect(http.StatusFound, "/50x")
|
||||
} else {
|
||||
ctx.Redirect(http.StatusFound, "/install")
|
||||
}
|
||||
}
|
||||
|
||||
// CheckConfigFile check config file if exist when installation
|
||||
// @Summary check config file if exist when installation
|
||||
// @Description check config file if exist when installation
|
||||
|
|
|
@ -34,6 +34,7 @@ func NewInstallHTTPServer() *gin.Engine {
|
|||
}))
|
||||
|
||||
installApi := r.Group("")
|
||||
installApi.GET("/", CheckConfigFileAndRedirectToInstallPage)
|
||||
installApi.GET("/install", WebPage)
|
||||
installApi.GET("/50x", WebPage)
|
||||
installApi.GET("/installation/language/options", LangOptions)
|
||||
|
|
|
@ -260,7 +260,7 @@ func initConfigTable(engine *xorm.Engine) error {
|
|||
{ID: 30, Key: "answer.vote_up", Value: `0`},
|
||||
{ID: 31, Key: "answer.vote_up_cancel", Value: `0`},
|
||||
{ID: 32, Key: "question.follow", Value: `0`},
|
||||
{ID: 33, Key: "email.config", Value: `{"from_name":"","from_email":"","smtp_host":"","smtp_port":465,"smtp_password":"","smtp_username":"","smtp_authentication":true,"encryption":"","register_title":"[{{.SiteName}}] Confirm your new account","register_body":"Welcome to {{.SiteName}}<br><br>\n\nClick the following link to confirm and activate your new account:<br>\n<a href='{{.RegisterUrl}}' target='_blank'>{{.RegisterUrl}}</a><br><br>\n\nIf the above link is not clickable, try copying and pasting it into the address bar of your web browser.\n","pass_reset_title":"[{{.SiteName }}] Password reset","pass_reset_body":"Somebody asked to reset your password on [{{.SiteName}}].<br><br>\n\nIf it was not you, you can safely ignore this email.<br><br>\n\nClick the following link to choose a new password:<br>\n<a href='{{.PassResetUrl}}' target='_blank'>{{.PassResetUrl}}</a>\n","change_title":"[{{.SiteName}}] Confirm your new email address","change_body":"Confirm your new email address for {{.SiteName}} by clicking on the following link:<br><br>\n\n<a href='{{.ChangeEmailUrl}}' target='_blank'>{{.ChangeEmailUrl}}</a><br><br>\n\nIf you did not request this change, please ignore this email.\n","test_title":"[{{.SiteName}}] Test Email","test_body":"This is a test email."}`},
|
||||
{ID: 33, Key: "email.config", Value: `{"from_name":"","from_email":"","smtp_host":"","smtp_port":465,"smtp_password":"","smtp_username":"","smtp_authentication":true,"encryption":"","register_title":"[{{.SiteName}}] Confirm your new account","register_body":"Welcome to {{.SiteName}}<br><br>\n\nClick the following link to confirm and activate your new account:<br>\n<a href='{{.RegisterUrl}}' target='_blank'>{{.RegisterUrl}}</a><br><br>\n\nIf the above link is not clickable, try copying and pasting it into the address bar of your web browser.\n","pass_reset_title":"[{{.SiteName }}] Password reset","pass_reset_body":"Somebody asked to reset your password on [{{.SiteName}}].<br><br>\n\nIf it was not you, you can safely ignore this email.<br><br>\n\nClick the following link to choose a new password:<br>\n<a href='{{.PassResetUrl}}' target='_blank'>{{.PassResetUrl}}</a>\n","change_title":"[{{.SiteName}}] Confirm your new email address","change_body":"Confirm your new email address for {{.SiteName}} by clicking on the following link:<br><br>\n\n<a href='{{.ChangeEmailUrl}}' target='_blank'>{{.ChangeEmailUrl}}</a><br><br>\n\nIf you did not request this change, please ignore this email.\n","test_title":"[{{.SiteName}}] Test Email","test_body":"This is a test email.","new_answer_title":"[{{.SiteName}}] {{.DisplayName}} answered your question","new_answer_body":"<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>","new_comment_title":"[{{.SiteName}}] {{.DisplayName}} commented on your post","new_comment_body":"<strong><a href='{{.CommentUrl}}'>{{.QuestionTitle}}</a></strong><br><br>\n\n<small>{{.DisplayName}}:</small><br>\n<blockquote>{{.CommentSummary}}</blockquote><br>\n<a href='{{.CommentUrl}}'>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>"}`},
|
||||
{ID: 35, Key: "tag.follow", Value: `0`},
|
||||
{ID: 36, Key: "rank.question.add", Value: `1`},
|
||||
{ID: 37, Key: "rank.question.edit", Value: `200`},
|
||||
|
|
|
@ -55,6 +55,7 @@ var migrations = []Migration{
|
|||
NewMigration("add activity timeline", addActivityTimeline, false),
|
||||
NewMigration("add user role", addRoleFeatures, false),
|
||||
NewMigration("add theme and private mode", addThemeAndPrivateMode, true),
|
||||
NewMigration("add new answer notification", addNewAnswerNotification, true),
|
||||
}
|
||||
|
||||
// GetCurrentDBVersion returns the current db version
|
||||
|
|
|
@ -25,6 +25,9 @@ func addThemeAndPrivateMode(x *xorm.Engine) error {
|
|||
}
|
||||
if !exist {
|
||||
_, err = x.InsertOne(siteInfo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("insert site info failed: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
themeConfig := `{"theme":"default","theme_config":{"default":{"navbar_style":"colored","primary_color":"#0033ff"}}}`
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
package migrations
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/answerdev/answer/internal/entity"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
func addNewAnswerNotification(x *xorm.Engine) error {
|
||||
cond := &entity.Config{Key: "email.config"}
|
||||
exists, err := x.Get(cond)
|
||||
if err != nil {
|
||||
return fmt.Errorf("get email config failed: %w", err)
|
||||
}
|
||||
if !exists {
|
||||
// This should be impossible except that the config was deleted manually by user.
|
||||
_, err = x.InsertOne(&entity.Config{
|
||||
Key: "email.config",
|
||||
Value: `{"from_name":"","from_email":"","smtp_host":"","smtp_port":465,"smtp_password":"","smtp_username":"","smtp_authentication":true,"encryption":"","register_title":"[{{.SiteName}}] Confirm your new account","register_body":"Welcome to {{.SiteName}}<br><br>\n\nClick the following link to confirm and activate your new account:<br>\n<a href='{{.RegisterUrl}}' target='_blank'>{{.RegisterUrl}}</a><br><br>\n\nIf the above link is not clickable, try copying and pasting it into the address bar of your web browser.\n","pass_reset_title":"[{{.SiteName }}] Password reset","pass_reset_body":"Somebody asked to reset your password on [{{.SiteName}}].<br><br>\n\nIf it was not you, you can safely ignore this email.<br><br>\n\nClick the following link to choose a new password:<br>\n<a href='{{.PassResetUrl}}' target='_blank'>{{.PassResetUrl}}</a>\n","change_title":"[{{.SiteName}}] Confirm your new email address","change_body":"Confirm your new email address for {{.SiteName}} by clicking on the following link:<br><br>\n\n<a href='{{.ChangeEmailUrl}}' target='_blank'>{{.ChangeEmailUrl}}</a><br><br>\n\nIf you did not request this change, please ignore this email.\n","test_title":"[{{.SiteName}}] Test Email","test_body":"This is a test email.","new_answer_title":"[{{.SiteName}}] {{.DisplayName}} answered your question","new_answer_body":"<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>","new_comment_title":"[{{.SiteName}}] {{.DisplayName}} commented on your post","new_comment_body":"<strong><a href='{{.CommentUrl}}'>{{.QuestionTitle}}</a></strong><br><br>\n\n<small>{{.DisplayName}}:</small><br>\n<blockquote>{{.CommentSummary}}</blockquote><br>\n<a href='{{.CommentUrl}}'>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>"}`,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("add email config failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
m := make(map[string]interface{})
|
||||
_ = json.Unmarshal([]byte(cond.Value), &m)
|
||||
m["new_answer_title"] = "[{{.SiteName}}] {{.DisplayName}} answered your question"
|
||||
m["new_answer_body"] = "<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>"
|
||||
m["new_comment_title"] = "[{{.SiteName}}] {{.DisplayName}} commented on your post"
|
||||
m["new_comment_body"] = "<strong><a href='{{.CommentUrl}}'>{{.QuestionTitle}}</a></strong><br><br>\n\n<small>{{.DisplayName}}:</small><br>\n<blockquote>{{.CommentSummary}}</blockquote><br>\n<a href='{{.CommentUrl}}'>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>"
|
||||
|
||||
val, _ := json.Marshal(m)
|
||||
_, err = x.ID(cond.ID).Update(&entity.Config{Value: string(val)})
|
||||
if err != nil {
|
||||
return fmt.Errorf("update email config failed: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -23,8 +23,8 @@ func NewEmailRepo(data *data.Data) export.EmailRepo {
|
|||
}
|
||||
|
||||
// SetCode The email code is used to verify that the link in the message is out of date
|
||||
func (e *emailRepo) SetCode(ctx context.Context, code, content string) error {
|
||||
err := e.data.Cache.SetString(ctx, code, content, 10*time.Minute)
|
||||
func (e *emailRepo) SetCode(ctx context.Context, code, content string, duration time.Duration) error {
|
||||
err := e.data.Cache.SetString(ctx, code, content, duration)
|
||||
if err != nil {
|
||||
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
|
||||
}
|
||||
|
|
|
@ -205,70 +205,40 @@ func (qr *questionRepo) GetQuestionIDsPage(ctx context.Context, page, pageSize i
|
|||
return questionIDList, nil
|
||||
}
|
||||
|
||||
// GetQuestionPage get question page
|
||||
func (qr *questionRepo) GetQuestionPage(ctx context.Context, page, pageSize int, question *entity.Question) (questionList []*entity.Question, total int64, err error) {
|
||||
// GetQuestionPage query question page
|
||||
func (qr *questionRepo) GetQuestionPage(ctx context.Context, page, pageSize int, userID, tagID, orderCond string) (
|
||||
questionList []*entity.Question, total int64, err error) {
|
||||
questionList = make([]*entity.Question, 0)
|
||||
total, err = pager.Help(page, pageSize, questionList, question, qr.data.DB.NewSession())
|
||||
if err != nil {
|
||||
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// SearchList
|
||||
func (qr *questionRepo) SearchList(ctx context.Context, search *schema.QuestionSearch) ([]*entity.QuestionTag, int64, error) {
|
||||
var count int64
|
||||
var err error
|
||||
rows := make([]*entity.QuestionTag, 0)
|
||||
if search.Page > 0 {
|
||||
search.Page = search.Page - 1
|
||||
} else {
|
||||
search.Page = 0
|
||||
session := qr.data.DB.Where("question.status = ? OR question.status = ?",
|
||||
entity.QuestionStatusAvailable, entity.QuestionStatusClosed)
|
||||
if len(tagID) > 0 {
|
||||
session.Join("LEFT", "tag_rel", "question.id = tag_rel.object_id")
|
||||
session.And("tag_rel.tag_id = ?", tagID)
|
||||
session.And("tag_rel.status = ?", entity.TagRelStatusAvailable)
|
||||
}
|
||||
if search.PageSize == 0 {
|
||||
search.PageSize = constant.DefaultPageSize
|
||||
if len(userID) > 0 {
|
||||
session.And("question.user_id = ?", userID)
|
||||
}
|
||||
offset := search.Page * search.PageSize
|
||||
session := qr.data.DB.Table("question")
|
||||
|
||||
if len(search.TagIDs) > 0 {
|
||||
session = session.Join("LEFT", "tag_rel", "question.id = tag_rel.object_id")
|
||||
session = session.And("tag_rel.tag_id =?", search.TagIDs[0])
|
||||
// session = session.In("tag_rel.tag_id ", search.TagIDs)
|
||||
session = session.And("tag_rel.status =?", entity.TagRelStatusAvailable)
|
||||
}
|
||||
|
||||
if len(search.UserID) > 0 {
|
||||
session = session.And("question.user_id = ?", search.UserID)
|
||||
}
|
||||
|
||||
session = session.In("question.status", []int{entity.QuestionStatusAvailable, entity.QuestionStatusClosed})
|
||||
// if search.Status > 0 {
|
||||
// session = session.And("question.status = ?", search.Status)
|
||||
// }
|
||||
// switch
|
||||
// newest, active,frequent,score,unanswered
|
||||
switch search.Order {
|
||||
switch orderCond {
|
||||
case "newest":
|
||||
session = session.OrderBy("question.created_at desc")
|
||||
session.OrderBy("question.created_at DESC")
|
||||
case "active":
|
||||
session = session.OrderBy("question.post_update_time desc,question.updated_at desc")
|
||||
session.OrderBy("question.post_update_time DESC, question.updated_at DESC")
|
||||
case "frequent":
|
||||
session = session.OrderBy("question.view_count desc")
|
||||
session.OrderBy("question.view_count DESC")
|
||||
case "score":
|
||||
session = session.OrderBy("question.vote_count desc,question.view_count desc")
|
||||
session.OrderBy("question.vote_count DESC, question.view_count DESC")
|
||||
case "unanswered":
|
||||
session = session.And("question.last_answer_id = 0")
|
||||
session = session.OrderBy("question.created_at desc")
|
||||
session.Where("question.last_answer_id = 0")
|
||||
session.OrderBy("question.created_at DESC")
|
||||
}
|
||||
session = session.Limit(search.PageSize, offset)
|
||||
session = session.Select("question.id,question.user_id,last_edit_user_id,question.title,question.original_text,question.parsed_text,question.status,question.view_count,question.unique_view_count,question.vote_count,question.answer_count,question.collection_count,question.follow_count,question.accepted_answer_id,question.last_answer_id,question.created_at,question.updated_at,question.post_update_time,question.revision_id")
|
||||
count, err = session.FindAndCount(&rows)
|
||||
|
||||
total, err = pager.Help(page, pageSize, &questionList, &entity.Question{}, session)
|
||||
if err != nil {
|
||||
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
|
||||
return rows, count, err
|
||||
}
|
||||
return rows, count, nil
|
||||
return questionList, total, err
|
||||
}
|
||||
|
||||
func (qr *questionRepo) AdminSearchList(ctx context.Context, search *schema.AdminQuestionSearch) ([]*entity.Question, int64, error) {
|
||||
|
|
|
@ -3,6 +3,7 @@ package repo_test
|
|||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/answerdev/answer/internal/repo/export"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
@ -11,7 +12,7 @@ import (
|
|||
func Test_emailRepo_VerifyCode(t *testing.T) {
|
||||
emailRepo := export.NewEmailRepo(testDataSource)
|
||||
code, content := "1111", "test"
|
||||
err := emailRepo.SetCode(context.TODO(), code, content)
|
||||
err := emailRepo.SetCode(context.TODO(), code, content, time.Minute)
|
||||
assert.NoError(t, err)
|
||||
|
||||
verifyContent, err := emailRepo.VerifyCode(context.TODO(), code)
|
||||
|
|
|
@ -29,7 +29,7 @@ var (
|
|||
"`question`.`id` as `question_id`",
|
||||
"`title`",
|
||||
"`parsed_text`",
|
||||
"`question`.`created_at`",
|
||||
"`question`.`created_at` as `created_at`",
|
||||
"`user_id`",
|
||||
"`vote_count`",
|
||||
"`answer_count`",
|
||||
|
@ -42,7 +42,7 @@ var (
|
|||
"`question_id`",
|
||||
"`question`.`title` as `title`",
|
||||
"`answer`.`parsed_text` as `parsed_text`",
|
||||
"`answer`.`created_at`",
|
||||
"`answer`.`created_at` as `created_at`",
|
||||
"`answer`.`user_id` as `user_id`",
|
||||
"`answer`.`vote_count` as `vote_count`",
|
||||
"0 as `answer_count`",
|
||||
|
@ -466,6 +466,7 @@ func (sr *searchRepo) parseResult(ctx context.Context, res []map[string][]byte)
|
|||
Where(builder.Eq{"tag_rel.object_id": r["question_id"]}).
|
||||
And(builder.Eq{"tag_rel.status": entity.TagRelStatusAvailable}).
|
||||
UseBool("recommend", "reserved").
|
||||
OrderBy("tag.recommend DESC, tag.reserved DESC, tag.id DESC").
|
||||
Find(&tagsEntity)
|
||||
|
||||
if err != nil {
|
||||
|
|
|
@ -62,16 +62,11 @@ func (tr *tagCommonRepo) GetTagListByName(ctx context.Context, name string, hasR
|
|||
if name != "" {
|
||||
session.Where("slug_name LIKE ?", name+"%")
|
||||
} else {
|
||||
session.UseBool("recommend")
|
||||
cond.Recommend = true
|
||||
}
|
||||
session.Where(builder.Eq{"status": entity.TagStatusAvailable})
|
||||
session.Asc("slug_name")
|
||||
// if !hasReserved {
|
||||
// cond.Reserved = false
|
||||
// session.UseBool("recommend", "reserved")
|
||||
// } else {
|
||||
session.UseBool("recommend")
|
||||
// }
|
||||
err = session.OrderBy("recommend desc,reserved desc,id desc").Find(&tagList, cond)
|
||||
if err != nil {
|
||||
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
|
||||
|
|
|
@ -106,6 +106,7 @@ func (a *AnswerAPIRouter) RegisterMustUnAuthAnswerAPIRouter(r *gin.RouterGroup)
|
|||
r.POST("/user/password/reset", a.userController.RetrievePassWord)
|
||||
r.POST("/user/password/replacement", a.userController.UseRePassWord)
|
||||
r.GET("/user/info", a.userController.GetUserInfoByUserID)
|
||||
r.PUT("/user/email/notification", a.userController.UserUnsubscribeEmailNotification)
|
||||
}
|
||||
|
||||
func (a *AnswerAPIRouter) RegisterUnAuthAnswerAPIRouter(r *gin.RouterGroup) {
|
||||
|
@ -123,8 +124,7 @@ func (a *AnswerAPIRouter) RegisterUnAuthAnswerAPIRouter(r *gin.RouterGroup) {
|
|||
|
||||
//question
|
||||
r.GET("/question/info", a.questionController.GetQuestion)
|
||||
r.POST("/question/search", a.questionController.SearchList)
|
||||
r.GET("/question/page", a.questionController.Index)
|
||||
r.GET("/question/page", a.questionController.QuestionPage)
|
||||
r.GET("/question/similar/tag", a.questionController.SimilarQuestion)
|
||||
r.GET("/personal/qa/top", a.questionController.UserTop)
|
||||
r.GET("/personal/question/page", a.questionController.UserList)
|
||||
|
@ -142,7 +142,6 @@ func (a *AnswerAPIRouter) RegisterUnAuthAnswerAPIRouter(r *gin.RouterGroup) {
|
|||
r.GET("/tags/following", a.tagController.GetFollowingTags)
|
||||
r.GET("/tag", a.tagController.GetTagInfo)
|
||||
r.GET("/tag/synonyms", a.tagController.GetTagSynonyms)
|
||||
r.GET("/question/index", a.questionController.Index)
|
||||
|
||||
//search
|
||||
r.GET("/search", a.searchController.Search)
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
package schema
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
const (
|
||||
AccountActivationSourceType SourceType = "account-activation"
|
||||
PasswordResetSourceType SourceType = "password-reset"
|
||||
ConfirmNewEmailSourceType SourceType = "password-reset"
|
||||
UnsubscribeSourceType SourceType = "unsubscribe"
|
||||
)
|
||||
|
||||
type SourceType string
|
||||
|
||||
type EmailCodeContent struct {
|
||||
SourceType SourceType `json:"source_type"`
|
||||
Email string `json:"e_mail"`
|
||||
UserID string `json:"user_id"`
|
||||
}
|
||||
|
||||
func (r *EmailCodeContent) ToJSONString() string {
|
||||
codeBytes, _ := json.Marshal(r)
|
||||
return string(codeBytes)
|
||||
}
|
||||
|
||||
func (r *EmailCodeContent) FromJSONString(data string) error {
|
||||
return json.Unmarshal([]byte(data), &r)
|
||||
}
|
||||
|
||||
type NewAnswerTemplateRawData struct {
|
||||
AnswerUserDisplayName string
|
||||
QuestionTitle string
|
||||
QuestionID string
|
||||
AnswerID string
|
||||
AnswerSummary string
|
||||
UnsubscribeCode string
|
||||
}
|
||||
|
||||
type NewAnswerTemplateData struct {
|
||||
SiteName string
|
||||
DisplayName string
|
||||
QuestionTitle string
|
||||
AnswerUrl string
|
||||
AnswerSummary string
|
||||
UnsubscribeUrl string
|
||||
}
|
||||
|
||||
type NewCommentTemplateRawData struct {
|
||||
CommentUserDisplayName string
|
||||
QuestionTitle string
|
||||
QuestionID string
|
||||
AnswerID string
|
||||
CommentID string
|
||||
CommentSummary string
|
||||
UnsubscribeCode string
|
||||
}
|
||||
|
||||
type NewCommentTemplateData struct {
|
||||
SiteName string
|
||||
DisplayName string
|
||||
QuestionTitle string
|
||||
CommentUrl string
|
||||
CommentSummary string
|
||||
UnsubscribeUrl string
|
||||
}
|
|
@ -1,6 +1,8 @@
|
|||
package schema
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/answerdev/answer/internal/base/validator"
|
||||
"github.com/answerdev/answer/pkg/converter"
|
||||
)
|
||||
|
@ -222,15 +224,66 @@ type UserQuestionInfo struct {
|
|||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
type QuestionSearch struct {
|
||||
Page int `json:"page" form:"page"` // Query number of pages
|
||||
PageSize int `json:"page_size" form:"page_size"` // Search page size
|
||||
Order string `json:"order" form:"order"` // Search order by
|
||||
// Tags []string `json:"tags" form:"tags"` // Search tag
|
||||
Tag string `json:"tag" form:"tag"` //Search tag
|
||||
TagIDs []string `json:"-" form:"-"` // Search tag
|
||||
UserName string `json:"username" form:"username"` // Search username
|
||||
UserID string `json:"-" form:"-"`
|
||||
const (
|
||||
QuestionOrderCondNewest = "newest"
|
||||
QuestionOrderCondActive = "active"
|
||||
QuestionOrderCondFrequent = "frequent"
|
||||
QuestionOrderCondScore = "score"
|
||||
QuestionOrderCondUnanswered = "unanswered"
|
||||
)
|
||||
|
||||
// QuestionPageReq query questions page
|
||||
type QuestionPageReq struct {
|
||||
Page int `validate:"omitempty,min=1" form:"page"`
|
||||
PageSize int `validate:"omitempty,min=1" form:"page_size"`
|
||||
OrderCond string `validate:"omitempty,oneof=newest active frequent score unanswered" form:"order"`
|
||||
Tag string `validate:"omitempty,gt=0,lte=100" form:"tag"`
|
||||
Username string `validate:"omitempty,gt=0,lte=100" form:"username"`
|
||||
|
||||
LoginUserID string `json:"-"`
|
||||
UserIDBeSearched string `json:"-"`
|
||||
TagID string `json:"-"`
|
||||
}
|
||||
|
||||
const (
|
||||
QuestionPageRespOperationTypeAsked = "question.operation_type.asked"
|
||||
QuestionPageRespOperationTypeAnswered = "question.operation_type.answered"
|
||||
QuestionPageRespOperationTypeModified = "question.operation_type.modified"
|
||||
)
|
||||
|
||||
type QuestionPageResp struct {
|
||||
ID string `json:"id" `
|
||||
Title string `json:"title"`
|
||||
UrlTitle string `json:"url_title"`
|
||||
Description string `json:"description"`
|
||||
Status int `json:"status"`
|
||||
Tags []*TagResp `json:"tags"`
|
||||
|
||||
// question statistical information
|
||||
ViewCount int `json:"view_count"`
|
||||
UniqueViewCount int `json:"unique_view_count"`
|
||||
VoteCount int `json:"vote_count"`
|
||||
AnswerCount int `json:"answer_count"`
|
||||
CollectionCount int `json:"collection_count"`
|
||||
FollowCount int `json:"follow_count"`
|
||||
|
||||
// answer information
|
||||
AcceptedAnswerID string `json:"accepted_answer_id"`
|
||||
LastAnswerID string `json:"last_answer_id"`
|
||||
LastAnsweredUserID string `json:"-"`
|
||||
LastAnsweredAt time.Time `json:"-"`
|
||||
|
||||
// operator information
|
||||
OperatedAt int64 `json:"operated_at"`
|
||||
Operator *QuestionPageRespOperator `json:"operator"`
|
||||
OperationType string `json:"operation_type"`
|
||||
}
|
||||
|
||||
type QuestionPageRespOperator struct {
|
||||
ID string `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Rank int `json:"rank"`
|
||||
DisplayName string `json:"display_name"`
|
||||
}
|
||||
|
||||
type AdminQuestionSearch struct {
|
||||
|
|
|
@ -26,6 +26,7 @@ type SearchObject struct {
|
|||
}
|
||||
|
||||
type TagResp struct {
|
||||
ID string `json:"-"`
|
||||
SlugName string `json:"slug_name"`
|
||||
DisplayName string `json:"display_name"`
|
||||
// if main tag slug name is not empty, this tag is synonymous with the main tag
|
||||
|
|
|
@ -3,10 +3,14 @@ package schema
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/mail"
|
||||
"net/url"
|
||||
|
||||
"github.com/answerdev/answer/internal/base/handler"
|
||||
"github.com/answerdev/answer/internal/base/reason"
|
||||
"github.com/answerdev/answer/internal/base/translator"
|
||||
"github.com/answerdev/answer/internal/base/validator"
|
||||
"github.com/segmentfault/pacman/errors"
|
||||
)
|
||||
|
||||
const PermaLinkQuestionIDAndTitle = 1
|
||||
|
@ -190,6 +194,17 @@ type UpdateSMTPConfigReq struct {
|
|||
TestEmailRecipient string `validate:"omitempty,email" json:"test_email_recipient"`
|
||||
}
|
||||
|
||||
func (r *UpdateSMTPConfigReq) Check() (errField []*validator.FormErrorField, err error) {
|
||||
_, err = mail.ParseAddress(r.FromName)
|
||||
if err == nil {
|
||||
return append(errField, &validator.FormErrorField{
|
||||
ErrorField: "from_name",
|
||||
ErrorMsg: reason.SMTPConfigFromNameCannotBeEmail,
|
||||
}), errors.BadRequest(reason.SMTPConfigFromNameCannotBeEmail)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// GetSMTPConfigResp get smtp config response
|
||||
type GetSMTPConfigResp struct {
|
||||
FromEmail string `json:"from_email"`
|
||||
|
|
|
@ -349,8 +349,8 @@ func (u *UserRePassWordRequest) Check() (errFields []*validator.FormErrorField,
|
|||
}
|
||||
|
||||
type UserNoticeSetRequest struct {
|
||||
UserID string `json:"-" ` // user_id
|
||||
NoticeSwitch bool `json:"notice_switch" `
|
||||
NoticeSwitch bool `json:"notice_switch"`
|
||||
UserID string `json:"-"`
|
||||
}
|
||||
|
||||
type UserNoticeSetResp struct {
|
||||
|
@ -396,20 +396,6 @@ type UserChangeEmailSendCodeReq struct {
|
|||
UserID string `json:"-"`
|
||||
}
|
||||
|
||||
type EmailCodeContent struct {
|
||||
Email string `json:"e_mail"`
|
||||
UserID string `json:"user_id"`
|
||||
}
|
||||
|
||||
func (r *EmailCodeContent) ToJSONString() string {
|
||||
codeBytes, _ := json.Marshal(r)
|
||||
return string(codeBytes)
|
||||
}
|
||||
|
||||
func (r *EmailCodeContent) FromJSONString(data string) error {
|
||||
return json.Unmarshal([]byte(data), &r)
|
||||
}
|
||||
|
||||
type UserChangeEmailVerifyReq struct {
|
||||
Code string `validate:"required,gt=0,lte=500" json:"code"`
|
||||
Content string `json:"-"`
|
||||
|
@ -440,3 +426,9 @@ type UserRankingSimpleInfo struct {
|
|||
// avatar
|
||||
Avatar string `json:"avatar"`
|
||||
}
|
||||
|
||||
// UserUnsubscribeEmailNotificationReq user unsubscribe email notification request
|
||||
type UserUnsubscribeEmailNotificationReq struct {
|
||||
Code string `validate:"required,gt=0,lte=500" json:"code"`
|
||||
Content string `json:"-"`
|
||||
}
|
||||
|
|
|
@ -15,11 +15,13 @@ import (
|
|||
"github.com/answerdev/answer/internal/service/activity_queue"
|
||||
answercommon "github.com/answerdev/answer/internal/service/answer_common"
|
||||
collectioncommon "github.com/answerdev/answer/internal/service/collection_common"
|
||||
"github.com/answerdev/answer/internal/service/export"
|
||||
"github.com/answerdev/answer/internal/service/notice_queue"
|
||||
"github.com/answerdev/answer/internal/service/permission"
|
||||
questioncommon "github.com/answerdev/answer/internal/service/question_common"
|
||||
"github.com/answerdev/answer/internal/service/revision_common"
|
||||
usercommon "github.com/answerdev/answer/internal/service/user_common"
|
||||
"github.com/answerdev/answer/pkg/encryption"
|
||||
"github.com/segmentfault/pacman/errors"
|
||||
"github.com/segmentfault/pacman/log"
|
||||
)
|
||||
|
@ -36,6 +38,7 @@ type AnswerService struct {
|
|||
revisionService *revision_common.RevisionService
|
||||
AnswerCommon *answercommon.AnswerCommon
|
||||
voteRepo activity_common.VoteRepo
|
||||
emailService *export.EmailService
|
||||
}
|
||||
|
||||
func NewAnswerService(
|
||||
|
@ -49,6 +52,7 @@ func NewAnswerService(
|
|||
answerAcceptActivityRepo *activity.AnswerActivityService,
|
||||
answerCommon *answercommon.AnswerCommon,
|
||||
voteRepo activity_common.VoteRepo,
|
||||
emailService *export.EmailService,
|
||||
) *AnswerService {
|
||||
return &AnswerService{
|
||||
answerRepo: answerRepo,
|
||||
|
@ -61,6 +65,7 @@ func NewAnswerService(
|
|||
answerActivityService: answerAcceptActivityRepo,
|
||||
AnswerCommon: answerCommon,
|
||||
voteRepo: voteRepo,
|
||||
emailService: emailService,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -176,7 +181,8 @@ func (as *AnswerService) Insert(ctx context.Context, req *schema.AnswerAddReq) (
|
|||
if err != nil {
|
||||
return insertData.ID, err
|
||||
}
|
||||
as.notificationAnswerTheQuestion(ctx, questionInfo.UserID, insertData.ID, req.UserID)
|
||||
as.notificationAnswerTheQuestion(ctx, questionInfo.UserID, questionInfo.ID, insertData.ID, req.UserID, questionInfo.Title,
|
||||
insertData.OriginalText)
|
||||
|
||||
activity_queue.AddActivity(&schema.ActivityMsg{
|
||||
UserID: insertData.UserID,
|
||||
|
@ -542,7 +548,12 @@ func (as *AnswerService) notificationUpdateAnswer(ctx context.Context, questionU
|
|||
notice_queue.AddNotification(msg)
|
||||
}
|
||||
|
||||
func (as *AnswerService) notificationAnswerTheQuestion(ctx context.Context, questionUserID, answerID, answerUserID string) {
|
||||
func (as *AnswerService) notificationAnswerTheQuestion(ctx context.Context,
|
||||
questionUserID, questionID, answerID, answerUserID, questionTitle, answerSummary string) {
|
||||
// If the question is answered by me, there is no notification for myself.
|
||||
if questionUserID == answerUserID {
|
||||
return
|
||||
}
|
||||
msg := &schema.NotificationMsg{
|
||||
TriggerUserID: answerUserID,
|
||||
ReceiverUserID: questionUserID,
|
||||
|
@ -552,4 +563,43 @@ func (as *AnswerService) notificationAnswerTheQuestion(ctx context.Context, ques
|
|||
msg.ObjectType = constant.AnswerObjectType
|
||||
msg.NotificationAction = constant.AnswerTheQuestion
|
||||
notice_queue.AddNotification(msg)
|
||||
|
||||
userInfo, exist, err := as.userRepo.GetByUserID(ctx, questionUserID)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return
|
||||
}
|
||||
if !exist {
|
||||
log.Warnf("user %s not found", questionUserID)
|
||||
return
|
||||
}
|
||||
if userInfo.NoticeStatus == schema.NoticeStatusOff || len(userInfo.EMail) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
rawData := &schema.NewAnswerTemplateRawData{
|
||||
QuestionTitle: questionTitle,
|
||||
QuestionID: questionID,
|
||||
AnswerID: answerID,
|
||||
AnswerSummary: answerSummary,
|
||||
UnsubscribeCode: encryption.MD5(userInfo.Pass),
|
||||
}
|
||||
answerUser, _, _ := as.userCommon.GetUserBasicInfoByID(ctx, answerUserID)
|
||||
if answerUser != nil {
|
||||
rawData.AnswerUserDisplayName = answerUser.DisplayName
|
||||
}
|
||||
codeContent := &schema.EmailCodeContent{
|
||||
SourceType: schema.UnsubscribeSourceType,
|
||||
Email: userInfo.EMail,
|
||||
UserID: userInfo.ID,
|
||||
}
|
||||
|
||||
title, body, err := as.emailService.NewAnswerTemplate(ctx, rawData)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
go as.emailService.SendAndSaveCodeWithTime(
|
||||
ctx, userInfo.EMail, title, body, rawData.UnsubscribeCode, codeContent.ToJSONString(), 7*24*time.Hour)
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ package comment
|
|||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/answerdev/answer/internal/base/constant"
|
||||
"github.com/answerdev/answer/internal/base/pager"
|
||||
|
@ -11,10 +12,12 @@ import (
|
|||
"github.com/answerdev/answer/internal/service/activity_common"
|
||||
"github.com/answerdev/answer/internal/service/activity_queue"
|
||||
"github.com/answerdev/answer/internal/service/comment_common"
|
||||
"github.com/answerdev/answer/internal/service/export"
|
||||
"github.com/answerdev/answer/internal/service/notice_queue"
|
||||
"github.com/answerdev/answer/internal/service/object_info"
|
||||
"github.com/answerdev/answer/internal/service/permission"
|
||||
usercommon "github.com/answerdev/answer/internal/service/user_common"
|
||||
"github.com/answerdev/answer/pkg/encryption"
|
||||
"github.com/jinzhu/copier"
|
||||
"github.com/segmentfault/pacman/errors"
|
||||
"github.com/segmentfault/pacman/log"
|
||||
|
@ -30,15 +33,6 @@ type CommentRepo interface {
|
|||
comments []*entity.Comment, total int64, err error)
|
||||
}
|
||||
|
||||
// CommentService user service
|
||||
type CommentService struct {
|
||||
commentRepo CommentRepo
|
||||
commentCommonRepo comment_common.CommentCommonRepo
|
||||
userCommon *usercommon.UserCommon
|
||||
voteCommon activity_common.VoteRepo
|
||||
objectInfoService *object_info.ObjService
|
||||
}
|
||||
|
||||
type CommentQuery struct {
|
||||
pager.PageCond
|
||||
// object id
|
||||
|
@ -59,19 +53,35 @@ func (c *CommentQuery) GetOrderBy() string {
|
|||
return "created_at ASC"
|
||||
}
|
||||
|
||||
// CommentService user service
|
||||
type CommentService struct {
|
||||
commentRepo CommentRepo
|
||||
commentCommonRepo comment_common.CommentCommonRepo
|
||||
userCommon *usercommon.UserCommon
|
||||
voteCommon activity_common.VoteRepo
|
||||
objectInfoService *object_info.ObjService
|
||||
emailService *export.EmailService
|
||||
userRepo usercommon.UserRepo
|
||||
}
|
||||
|
||||
// NewCommentService new comment service
|
||||
func NewCommentService(
|
||||
commentRepo CommentRepo,
|
||||
commentCommonRepo comment_common.CommentCommonRepo,
|
||||
userCommon *usercommon.UserCommon,
|
||||
objectInfoService *object_info.ObjService,
|
||||
voteCommon activity_common.VoteRepo) *CommentService {
|
||||
voteCommon activity_common.VoteRepo,
|
||||
emailService *export.EmailService,
|
||||
userRepo usercommon.UserRepo,
|
||||
) *CommentService {
|
||||
return &CommentService{
|
||||
commentRepo: commentRepo,
|
||||
commentCommonRepo: commentCommonRepo,
|
||||
userCommon: userCommon,
|
||||
voteCommon: voteCommon,
|
||||
objectInfoService: objectInfoService,
|
||||
emailService: emailService,
|
||||
userRepo: userRepo,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -112,9 +122,11 @@ func (cs *CommentService) AddComment(ctx context.Context, req *schema.AddComment
|
|||
}
|
||||
|
||||
if objInfo.ObjectType == constant.QuestionObjectType {
|
||||
cs.notificationQuestionComment(ctx, objInfo.ObjectCreatorUserID, comment.ID, req.UserID)
|
||||
cs.notificationQuestionComment(ctx, objInfo.ObjectCreatorUserID,
|
||||
objInfo.QuestionID, objInfo.Title, comment.ID, req.UserID, comment.OriginalText)
|
||||
} else if objInfo.ObjectType == constant.AnswerObjectType {
|
||||
cs.notificationAnswerComment(ctx, objInfo.ObjectCreatorUserID, comment.ID, req.UserID)
|
||||
cs.notificationAnswerComment(ctx, objInfo.QuestionID, objInfo.Title, objInfo.AnswerID,
|
||||
objInfo.ObjectCreatorUserID, comment.ID, req.UserID, comment.OriginalText)
|
||||
}
|
||||
if len(req.MentionUsernameList) > 0 {
|
||||
cs.notificationMention(ctx, req.MentionUsernameList, comment.ID, req.UserID)
|
||||
|
@ -331,21 +343,6 @@ func (cs *CommentService) convertCommentEntity2Resp(ctx context.Context, req *sc
|
|||
return commentResp, nil
|
||||
}
|
||||
|
||||
func (cs *CommentService) checkCommentWhetherOwner(ctx context.Context, userID, commentID string) error {
|
||||
// check comment if user self
|
||||
comment, exist, err := cs.commentCommonRepo.GetComment(ctx, commentID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !exist {
|
||||
return errors.BadRequest(reason.CommentNotFound)
|
||||
}
|
||||
if comment.UserID != userID {
|
||||
return errors.BadRequest(reason.CommentEditWithoutPermission)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cs *CommentService) checkIsVote(ctx context.Context, userID, commentID string) (isVote bool) {
|
||||
status := cs.voteCommon.GetVoteStatus(ctx, commentID, userID)
|
||||
return len(status) > 0
|
||||
|
@ -401,7 +398,11 @@ func (cs *CommentService) GetCommentPersonalWithPage(ctx context.Context, req *s
|
|||
return pager.NewPageModel(total, resp), nil
|
||||
}
|
||||
|
||||
func (cs *CommentService) notificationQuestionComment(ctx context.Context, questionUserID, commentID, commentUserID string) {
|
||||
func (cs *CommentService) notificationQuestionComment(ctx context.Context, questionUserID,
|
||||
questionID, questionTitle, commentID, commentUserID, commentSummary string) {
|
||||
if questionUserID == commentUserID {
|
||||
return
|
||||
}
|
||||
msg := &schema.NotificationMsg{
|
||||
ReceiverUserID: questionUserID,
|
||||
TriggerUserID: commentUserID,
|
||||
|
@ -411,9 +412,52 @@ func (cs *CommentService) notificationQuestionComment(ctx context.Context, quest
|
|||
msg.ObjectType = constant.CommentObjectType
|
||||
msg.NotificationAction = constant.CommentQuestion
|
||||
notice_queue.AddNotification(msg)
|
||||
|
||||
receiverUserInfo, exist, err := cs.userRepo.GetByUserID(ctx, questionUserID)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return
|
||||
}
|
||||
if !exist {
|
||||
log.Warnf("user %s not found", questionUserID)
|
||||
return
|
||||
}
|
||||
if receiverUserInfo.NoticeStatus == schema.NoticeStatusOff || len(receiverUserInfo.EMail) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
rawData := &schema.NewCommentTemplateRawData{
|
||||
QuestionTitle: questionTitle,
|
||||
QuestionID: questionID,
|
||||
CommentID: commentID,
|
||||
CommentSummary: commentSummary,
|
||||
UnsubscribeCode: encryption.MD5(receiverUserInfo.Pass),
|
||||
}
|
||||
commentUser, _, _ := cs.userCommon.GetUserBasicInfoByID(ctx, commentUserID)
|
||||
if commentUser != nil {
|
||||
rawData.CommentUserDisplayName = commentUser.DisplayName
|
||||
}
|
||||
codeContent := &schema.EmailCodeContent{
|
||||
SourceType: schema.UnsubscribeSourceType,
|
||||
Email: receiverUserInfo.EMail,
|
||||
UserID: receiverUserInfo.ID,
|
||||
}
|
||||
|
||||
title, body, err := cs.emailService.NewCommentTemplate(ctx, rawData)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
go cs.emailService.SendAndSaveCodeWithTime(
|
||||
ctx, receiverUserInfo.EMail, title, body, rawData.UnsubscribeCode, codeContent.ToJSONString(), 7*24*time.Hour)
|
||||
}
|
||||
|
||||
func (cs *CommentService) notificationAnswerComment(ctx context.Context, answerUserID, commentID, commentUserID string) {
|
||||
func (cs *CommentService) notificationAnswerComment(ctx context.Context,
|
||||
questionID, questionTitle, answerID, answerUserID, commentID, commentUserID, commentSummary string) {
|
||||
if answerUserID == commentUserID {
|
||||
return
|
||||
}
|
||||
msg := &schema.NotificationMsg{
|
||||
ReceiverUserID: answerUserID,
|
||||
TriggerUserID: commentUserID,
|
||||
|
@ -423,6 +467,46 @@ func (cs *CommentService) notificationAnswerComment(ctx context.Context, answerU
|
|||
msg.ObjectType = constant.CommentObjectType
|
||||
msg.NotificationAction = constant.CommentAnswer
|
||||
notice_queue.AddNotification(msg)
|
||||
|
||||
receiverUserInfo, exist, err := cs.userRepo.GetByUserID(ctx, answerUserID)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return
|
||||
}
|
||||
if !exist {
|
||||
log.Warnf("user %s not found", answerUserID)
|
||||
return
|
||||
}
|
||||
if receiverUserInfo.NoticeStatus == schema.NoticeStatusOff || len(receiverUserInfo.EMail) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
rawData := &schema.NewCommentTemplateRawData{
|
||||
QuestionTitle: questionTitle,
|
||||
QuestionID: questionID,
|
||||
AnswerID: answerID,
|
||||
CommentID: commentID,
|
||||
CommentSummary: commentSummary,
|
||||
UnsubscribeCode: encryption.MD5(receiverUserInfo.Pass),
|
||||
}
|
||||
commentUser, _, _ := cs.userCommon.GetUserBasicInfoByID(ctx, commentUserID)
|
||||
if commentUser != nil {
|
||||
rawData.CommentUserDisplayName = commentUser.DisplayName
|
||||
}
|
||||
codeContent := &schema.EmailCodeContent{
|
||||
SourceType: schema.UnsubscribeSourceType,
|
||||
Email: receiverUserInfo.EMail,
|
||||
UserID: receiverUserInfo.ID,
|
||||
}
|
||||
|
||||
title, body, err := cs.emailService.NewCommentTemplate(ctx, rawData)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
go cs.emailService.SendAndSaveCodeWithTime(
|
||||
ctx, receiverUserInfo.EMail, title, body, rawData.UnsubscribeCode, codeContent.ToJSONString(), 7*24*time.Hour)
|
||||
}
|
||||
|
||||
func (cs *CommentService) notificationCommentReply(ctx context.Context, replyUserID, commentID, commentUserID string) {
|
||||
|
|
|
@ -6,6 +6,7 @@ import (
|
|||
"fmt"
|
||||
"html/template"
|
||||
"mime"
|
||||
"time"
|
||||
|
||||
"github.com/answerdev/answer/internal/base/reason"
|
||||
"github.com/answerdev/answer/internal/entity"
|
||||
|
@ -27,7 +28,7 @@ type EmailService struct {
|
|||
|
||||
// EmailRepo email repository
|
||||
type EmailRepo interface {
|
||||
SetCode(ctx context.Context, code, content string) error
|
||||
SetCode(ctx context.Context, code, content string, duration time.Duration) error
|
||||
VerifyCode(ctx context.Context, code string) (content string, err error)
|
||||
}
|
||||
|
||||
|
@ -51,14 +52,18 @@ type EmailConfig struct {
|
|||
SMTPPassword string `json:"smtp_password"`
|
||||
SMTPAuthentication bool `json:"smtp_authentication"`
|
||||
|
||||
RegisterTitle string `json:"register_title"`
|
||||
RegisterBody string `json:"register_body"`
|
||||
PassResetTitle string `json:"pass_reset_title"`
|
||||
PassResetBody string `json:"pass_reset_body"`
|
||||
ChangeTitle string `json:"change_title"`
|
||||
ChangeBody string `json:"change_body"`
|
||||
TestTitle string `json:"test_title"`
|
||||
TestBody string `json:"test_body"`
|
||||
RegisterTitle string `json:"register_title"`
|
||||
RegisterBody string `json:"register_body"`
|
||||
PassResetTitle string `json:"pass_reset_title"`
|
||||
PassResetBody string `json:"pass_reset_body"`
|
||||
ChangeTitle string `json:"change_title"`
|
||||
ChangeBody string `json:"change_body"`
|
||||
TestTitle string `json:"test_title"`
|
||||
TestBody string `json:"test_body"`
|
||||
NewAnswerTitle string `json:"new_answer_title"`
|
||||
NewAnswerBody string `json:"new_answer_body"`
|
||||
NewCommentTitle string `json:"new_comment_title"`
|
||||
NewCommentBody string `json:"new_comment_body"`
|
||||
}
|
||||
|
||||
func (e *EmailConfig) IsSSL() bool {
|
||||
|
@ -84,8 +89,27 @@ type TestTemplateData struct {
|
|||
SiteName string
|
||||
}
|
||||
|
||||
// SendAndSaveCode send email and save code
|
||||
func (es *EmailService) SendAndSaveCode(ctx context.Context, toEmailAddr, subject, body, code, codeContent string) {
|
||||
es.Send(ctx, toEmailAddr, subject, body)
|
||||
err := es.emailRepo.SetCode(ctx, code, codeContent, 10*time.Minute)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
// SendAndSaveCodeWithTime send email and save code
|
||||
func (es *EmailService) SendAndSaveCodeWithTime(
|
||||
ctx context.Context, toEmailAddr, subject, body, code, codeContent string, duration time.Duration) {
|
||||
es.Send(ctx, toEmailAddr, subject, body)
|
||||
err := es.emailRepo.SetCode(ctx, code, codeContent, duration)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Send email send
|
||||
func (es *EmailService) Send(ctx context.Context, toEmailAddr, subject, body, code, codeContent string) {
|
||||
func (es *EmailService) Send(ctx context.Context, toEmailAddr, subject, body string) {
|
||||
log.Infof("try to send email to %s", toEmailAddr)
|
||||
ec, err := es.GetEmailConfig()
|
||||
if err != nil {
|
||||
|
@ -109,13 +133,6 @@ func (es *EmailService) Send(ctx context.Context, toEmailAddr, subject, body, co
|
|||
} else {
|
||||
log.Infof("send email to %s success", toEmailAddr)
|
||||
}
|
||||
|
||||
if len(code) > 0 {
|
||||
err = es.emailRepo.SetCode(ctx, code, codeContent)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// VerifyUrlExpired email send
|
||||
|
@ -250,41 +267,118 @@ func (es *EmailService) ChangeEmailTemplate(ctx context.Context, changeEmailUrl
|
|||
return titleBuf.String(), bodyBuf.String(), nil
|
||||
}
|
||||
|
||||
// TestTemplate send test email template parse
|
||||
func (es *EmailService) TestTemplate(ctx context.Context) (title, body string, err error) {
|
||||
ec, err := es.GetEmailConfig()
|
||||
emailConfig, err := es.GetEmailConfig()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
siteinfo, err := es.GetSiteGeneral(ctx)
|
||||
siteInfo, err := es.GetSiteGeneral(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
templateData := TestTemplateData{
|
||||
SiteName: siteinfo.Name,
|
||||
SiteName: siteInfo.Name,
|
||||
}
|
||||
|
||||
titleBuf := &bytes.Buffer{}
|
||||
bodyBuf := &bytes.Buffer{}
|
||||
title, err = es.parseTemplateData(emailConfig.TestTitle, templateData)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("email template parse error: %s", err)
|
||||
}
|
||||
|
||||
tmpl, err := template.New("test_title").Parse(ec.TestTitle)
|
||||
body, err = es.parseTemplateData(emailConfig.TestBody, templateData)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("email test title template parse error: %s", err)
|
||||
return "", "", fmt.Errorf("email template parse error: %s", err)
|
||||
}
|
||||
err = tmpl.Execute(titleBuf, templateData)
|
||||
return title, body, nil
|
||||
}
|
||||
|
||||
// NewAnswerTemplate new answer template
|
||||
func (es *EmailService) NewAnswerTemplate(ctx context.Context, raw *schema.NewAnswerTemplateRawData) (
|
||||
title, body string, err error) {
|
||||
emailConfig, err := es.GetEmailConfig()
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("email test body template parse error: %s", err)
|
||||
return
|
||||
}
|
||||
tmpl, err = template.New("test_body").Parse(ec.TestBody)
|
||||
|
||||
siteInfo, err := es.GetSiteGeneral(ctx)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("test_body template parse error: %s", err)
|
||||
return
|
||||
}
|
||||
err = tmpl.Execute(bodyBuf, templateData)
|
||||
templateData := &schema.NewAnswerTemplateData{
|
||||
SiteName: siteInfo.Name,
|
||||
DisplayName: raw.AnswerUserDisplayName,
|
||||
QuestionTitle: raw.QuestionTitle,
|
||||
AnswerUrl: fmt.Sprintf("%s/questions/%s/%s", siteInfo.SiteUrl, raw.QuestionID, raw.AnswerID),
|
||||
AnswerSummary: raw.AnswerSummary,
|
||||
UnsubscribeUrl: fmt.Sprintf("%s/users/unsubscribe?code=%s", siteInfo.SiteUrl, raw.UnsubscribeCode),
|
||||
}
|
||||
templateData.SiteName = siteInfo.Name
|
||||
|
||||
title, err = es.parseTemplateData(emailConfig.NewAnswerTitle, templateData)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
return "", "", fmt.Errorf("email template parse error: %s", err)
|
||||
}
|
||||
return titleBuf.String(), bodyBuf.String(), nil
|
||||
|
||||
body, err = es.parseTemplateData(emailConfig.NewAnswerBody, templateData)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("email template parse error: %s", err)
|
||||
}
|
||||
return title, body, nil
|
||||
}
|
||||
|
||||
// NewCommentTemplate new comment template
|
||||
func (es *EmailService) NewCommentTemplate(ctx context.Context, raw *schema.NewCommentTemplateRawData) (
|
||||
title, body string, err error) {
|
||||
emailConfig, err := es.GetEmailConfig()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
siteInfo, err := es.GetSiteGeneral(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
templateData := &schema.NewCommentTemplateData{
|
||||
SiteName: siteInfo.Name,
|
||||
DisplayName: raw.CommentUserDisplayName,
|
||||
QuestionTitle: raw.QuestionTitle,
|
||||
CommentSummary: raw.CommentSummary,
|
||||
UnsubscribeUrl: fmt.Sprintf("%s/users/unsubscribe?code=%s", siteInfo.SiteUrl, raw.UnsubscribeCode),
|
||||
}
|
||||
if len(raw.AnswerID) > 0 {
|
||||
templateData.CommentUrl = fmt.Sprintf("%s/questions/%s/%s?commentId=%s", siteInfo.SiteUrl, raw.QuestionID,
|
||||
raw.AnswerID, raw.CommentID)
|
||||
} else {
|
||||
templateData.CommentUrl = fmt.Sprintf("%s/questions/%s?commentId=%s", siteInfo.SiteUrl,
|
||||
raw.QuestionID, raw.CommentID)
|
||||
}
|
||||
templateData.SiteName = siteInfo.Name
|
||||
|
||||
title, err = es.parseTemplateData(emailConfig.NewCommentTitle, templateData)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("email template parse error: %s", err)
|
||||
}
|
||||
|
||||
body, err = es.parseTemplateData(emailConfig.NewCommentBody, templateData)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("email template parse error: %s", err)
|
||||
}
|
||||
return title, body, nil
|
||||
}
|
||||
|
||||
func (es *EmailService) parseTemplateData(templateContent string, templateData interface{}) (parsedData string, err error) {
|
||||
parsedDataBuf := &bytes.Buffer{}
|
||||
tmpl, err := template.New("").Parse(templateContent)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
err = tmpl.Execute(parsedDataBuf, templateData)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return parsedDataBuf.String(), nil
|
||||
}
|
||||
|
||||
func (es *EmailService) GetEmailConfig() (ec *EmailConfig, err error) {
|
||||
|
|
|
@ -73,7 +73,7 @@ func (ns *NotificationCommon) HandleNotification() {
|
|||
|
||||
// AddNotification
|
||||
// need set
|
||||
// UserID
|
||||
// LoginUserID
|
||||
// Type 1 inbox 2 achievement
|
||||
// [inbox] Activity
|
||||
// [achievement] Rank
|
||||
|
|
|
@ -6,11 +6,14 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/answerdev/answer/internal/base/constant"
|
||||
"github.com/answerdev/answer/internal/base/handler"
|
||||
"github.com/answerdev/answer/internal/base/reason"
|
||||
"github.com/answerdev/answer/internal/base/translator"
|
||||
"github.com/answerdev/answer/internal/service/activity_common"
|
||||
"github.com/answerdev/answer/internal/service/activity_queue"
|
||||
"github.com/answerdev/answer/internal/service/config"
|
||||
"github.com/answerdev/answer/internal/service/meta"
|
||||
"github.com/answerdev/answer/pkg/checker"
|
||||
"github.com/answerdev/answer/pkg/htmltext"
|
||||
"github.com/segmentfault/pacman/errors"
|
||||
|
||||
|
@ -30,8 +33,8 @@ type QuestionRepo interface {
|
|||
UpdateQuestion(ctx context.Context, question *entity.Question, Cols []string) (err error)
|
||||
GetQuestion(ctx context.Context, id string) (question *entity.Question, exist bool, err error)
|
||||
GetQuestionList(ctx context.Context, question *entity.Question) (questions []*entity.Question, err error)
|
||||
GetQuestionPage(ctx context.Context, page, pageSize int, question *entity.Question) (questions []*entity.Question, total int64, err error)
|
||||
SearchList(ctx context.Context, search *schema.QuestionSearch) ([]*entity.QuestionTag, int64, error)
|
||||
GetQuestionPage(ctx context.Context, page, pageSize int, userID, tagID, orderCond string) (
|
||||
questionList []*entity.Question, total int64, err error)
|
||||
UpdateQuestionStatus(ctx context.Context, question *entity.Question) (err error)
|
||||
SearchByTitleLike(ctx context.Context, title string) (questionList []*entity.Question, err error)
|
||||
UpdatePvCount(ctx context.Context, questionID string) (err error)
|
||||
|
@ -126,21 +129,15 @@ func (qs *QuestionCommon) UpdataPostSetTime(ctx context.Context, questionID stri
|
|||
|
||||
func (qs *QuestionCommon) FindInfoByID(ctx context.Context, questionIDs []string, loginUserID string) (map[string]*schema.QuestionInfo, error) {
|
||||
list := make(map[string]*schema.QuestionInfo)
|
||||
listAddTag := make([]*entity.QuestionTag, 0)
|
||||
questionList, err := qs.questionRepo.FindByID(ctx, questionIDs)
|
||||
if err != nil {
|
||||
return list, err
|
||||
}
|
||||
for _, item := range questionList {
|
||||
itemAddTag := &entity.QuestionTag{}
|
||||
itemAddTag.Question = *item
|
||||
listAddTag = append(listAddTag, itemAddTag)
|
||||
}
|
||||
QuestionInfo, err := qs.ListFormat(ctx, listAddTag, loginUserID)
|
||||
questions, err := qs.FormatQuestions(ctx, questionList, loginUserID)
|
||||
if err != nil {
|
||||
return list, err
|
||||
}
|
||||
for _, item := range QuestionInfo {
|
||||
for _, item := range questions {
|
||||
list[item.ID] = item
|
||||
}
|
||||
return list, nil
|
||||
|
@ -193,9 +190,15 @@ func (qs *QuestionCommon) Info(ctx context.Context, questionID string, loginUser
|
|||
showinfo.Tags = tagmap
|
||||
|
||||
userIds := make([]string, 0)
|
||||
userIds = append(userIds, dbinfo.UserID)
|
||||
userIds = append(userIds, dbinfo.LastEditUserID)
|
||||
userIds = append(userIds, showinfo.LastAnsweredUserID)
|
||||
if checker.IsNotZeroString(dbinfo.UserID) {
|
||||
userIds = append(userIds, dbinfo.UserID)
|
||||
}
|
||||
if checker.IsNotZeroString(dbinfo.LastEditUserID) {
|
||||
userIds = append(userIds, dbinfo.LastEditUserID)
|
||||
}
|
||||
if checker.IsNotZeroString(showinfo.LastAnsweredUserID) {
|
||||
userIds = append(userIds, showinfo.LastAnsweredUserID)
|
||||
}
|
||||
userInfoMap, err := qs.userCommon.BatchUserBasicInfoByID(ctx, userIds)
|
||||
if err != nil {
|
||||
return showinfo, err
|
||||
|
@ -244,13 +247,114 @@ func (qs *QuestionCommon) Info(ctx context.Context, questionID string, loginUser
|
|||
return showinfo, nil
|
||||
}
|
||||
|
||||
func (qs *QuestionCommon) ListFormat(ctx context.Context, questionList []*entity.QuestionTag, loginUserID string) ([]*schema.QuestionInfo, error) {
|
||||
func (qs *QuestionCommon) FormatQuestionsPage(
|
||||
ctx context.Context, questionList []*entity.Question, loginUserID string, orderCond string) (
|
||||
formattedQuestions []*schema.QuestionPageResp, err error) {
|
||||
language := handler.GetLangByCtx(ctx)
|
||||
askedOp := translator.GlobalTrans.Tr(language, schema.QuestionPageRespOperationTypeAsked)
|
||||
answeredOp := translator.GlobalTrans.Tr(language, schema.QuestionPageRespOperationTypeAnswered)
|
||||
modifiedOp := translator.GlobalTrans.Tr(language, schema.QuestionPageRespOperationTypeModified)
|
||||
|
||||
formattedQuestions = make([]*schema.QuestionPageResp, 0)
|
||||
|
||||
questionIDs := make([]string, 0)
|
||||
userIDs := make([]string, 0)
|
||||
for _, questionInfo := range questionList {
|
||||
t := &schema.QuestionPageResp{
|
||||
ID: questionInfo.ID,
|
||||
Title: questionInfo.Title,
|
||||
UrlTitle: htmltext.UrlTitle(questionInfo.Title),
|
||||
Description: htmltext.FetchExcerpt(questionInfo.ParsedText, "...", 240),
|
||||
Status: questionInfo.Status,
|
||||
ViewCount: questionInfo.ViewCount,
|
||||
UniqueViewCount: questionInfo.UniqueViewCount,
|
||||
VoteCount: questionInfo.VoteCount,
|
||||
AnswerCount: questionInfo.AnswerCount,
|
||||
CollectionCount: questionInfo.CollectionCount,
|
||||
FollowCount: questionInfo.FollowCount,
|
||||
AcceptedAnswerID: questionInfo.AcceptedAnswerID,
|
||||
LastAnswerID: questionInfo.LastAnswerID,
|
||||
}
|
||||
|
||||
questionIDs = append(questionIDs, questionInfo.ID)
|
||||
userIDs = append(userIDs, questionInfo.UserID)
|
||||
haveEdited, haveAnswered := false, false
|
||||
if checker.IsNotZeroString(questionInfo.LastEditUserID) {
|
||||
haveEdited = true
|
||||
userIDs = append(userIDs, questionInfo.LastEditUserID)
|
||||
}
|
||||
if checker.IsNotZeroString(questionInfo.LastAnswerID) {
|
||||
haveAnswered = true
|
||||
|
||||
answerInfo, exist, err := qs.answerRepo.GetAnswer(ctx, questionInfo.LastAnswerID)
|
||||
if err == nil && exist {
|
||||
if answerInfo.LastEditUserID != "0" {
|
||||
t.LastAnsweredUserID = answerInfo.LastEditUserID
|
||||
} else {
|
||||
t.LastAnsweredUserID = answerInfo.UserID
|
||||
}
|
||||
t.LastAnsweredAt = answerInfo.CreatedAt
|
||||
userIDs = append(userIDs, t.LastAnsweredUserID)
|
||||
}
|
||||
}
|
||||
|
||||
// if order condition is newest or nobody edited or nobody answered, only show question author
|
||||
if orderCond == schema.QuestionOrderCondNewest || (!haveEdited && !haveAnswered) {
|
||||
t.OperationType = askedOp
|
||||
t.OperatedAt = questionInfo.CreatedAt.Unix()
|
||||
t.Operator = &schema.QuestionPageRespOperator{ID: questionInfo.UserID}
|
||||
} else {
|
||||
// if no one
|
||||
if haveEdited {
|
||||
t.OperationType = modifiedOp
|
||||
t.OperatedAt = questionInfo.UpdatedAt.Unix()
|
||||
t.Operator = &schema.QuestionPageRespOperator{ID: questionInfo.LastEditUserID}
|
||||
}
|
||||
|
||||
if haveAnswered {
|
||||
if t.LastAnsweredAt.Unix() > t.OperatedAt {
|
||||
t.OperationType = answeredOp
|
||||
t.OperatedAt = t.LastAnsweredAt.Unix()
|
||||
t.Operator = &schema.QuestionPageRespOperator{ID: t.LastAnsweredUserID}
|
||||
}
|
||||
}
|
||||
}
|
||||
formattedQuestions = append(formattedQuestions, t)
|
||||
}
|
||||
|
||||
tagsMap, err := qs.tagCommon.BatchGetObjectTag(ctx, questionIDs)
|
||||
if err != nil {
|
||||
return formattedQuestions, err
|
||||
}
|
||||
userInfoMap, err := qs.userCommon.BatchUserBasicInfoByID(ctx, userIDs)
|
||||
if err != nil {
|
||||
return formattedQuestions, err
|
||||
}
|
||||
|
||||
for _, item := range formattedQuestions {
|
||||
tags, ok := tagsMap[item.ID]
|
||||
if ok {
|
||||
item.Tags = tags
|
||||
} else {
|
||||
item.Tags = make([]*schema.TagResp, 0)
|
||||
}
|
||||
userInfo := userInfoMap[item.Operator.ID]
|
||||
if userInfo != nil {
|
||||
item.Operator.DisplayName = userInfo.DisplayName
|
||||
item.Operator.Username = userInfo.Username
|
||||
item.Operator.Rank = userInfo.Rank
|
||||
}
|
||||
}
|
||||
return formattedQuestions, nil
|
||||
}
|
||||
|
||||
func (qs *QuestionCommon) FormatQuestions(ctx context.Context, questionList []*entity.Question, loginUserID string) ([]*schema.QuestionInfo, error) {
|
||||
list := make([]*schema.QuestionInfo, 0)
|
||||
objectIds := make([]string, 0)
|
||||
userIds := make([]string, 0)
|
||||
|
||||
for _, questionInfo := range questionList {
|
||||
item := qs.ShowListFormat(ctx, questionInfo)
|
||||
item := qs.ShowFormat(ctx, questionInfo)
|
||||
list = append(list, item)
|
||||
objectIds = append(objectIds, item.ID)
|
||||
userIds = append(userIds, item.UserID)
|
||||
|
@ -387,8 +491,8 @@ func (as *QuestionCommon) RemoveAnswer(ctx context.Context, id string) (err erro
|
|||
return as.answerRepo.RemoveAnswer(ctx, id)
|
||||
}
|
||||
|
||||
func (qs *QuestionCommon) ShowListFormat(ctx context.Context, data *entity.QuestionTag) *schema.QuestionInfo {
|
||||
return qs.ShowFormat(ctx, &data.Question)
|
||||
func (qs *QuestionCommon) ShowListFormat(ctx context.Context, data *entity.Question) *schema.QuestionInfo {
|
||||
return qs.ShowFormat(ctx, data)
|
||||
}
|
||||
|
||||
func (qs *QuestionCommon) ShowFormat(ctx context.Context, data *entity.Question) *schema.QuestionInfo {
|
||||
|
|
|
@ -476,11 +476,7 @@ func (qs *QuestionService) UpdateQuestion(ctx context.Context, req *schema.Quest
|
|||
question.UpdatedAt = now
|
||||
question.PostUpdateTime = now
|
||||
question.UserID = dbinfo.UserID
|
||||
|
||||
question.LastEditUserID = "0"
|
||||
if dbinfo.UserID != req.UserID {
|
||||
question.LastEditUserID = req.UserID
|
||||
}
|
||||
question.LastEditUserID = req.UserID
|
||||
|
||||
oldTags, tagerr := qs.tagCommon.GetObjectEntityTag(ctx, question.ID)
|
||||
if tagerr != nil {
|
||||
|
@ -655,12 +651,13 @@ func (qs *QuestionService) SearchUserList(ctx context.Context, userName, order s
|
|||
if !Exist {
|
||||
return userlist, 0, nil
|
||||
}
|
||||
search := &schema.QuestionSearch{}
|
||||
search.Order = order
|
||||
search := &schema.QuestionPageReq{}
|
||||
search.OrderCond = order
|
||||
search.Page = page
|
||||
search.PageSize = pageSize
|
||||
search.UserID = userinfo.ID
|
||||
questionlist, count, err := qs.SearchList(ctx, search, loginUserID)
|
||||
search.UserIDBeSearched = userinfo.ID
|
||||
search.LoginUserID = loginUserID
|
||||
questionlist, count, err := qs.GetQuestionPage(ctx, search)
|
||||
if err != nil {
|
||||
return userlist, 0, err
|
||||
}
|
||||
|
@ -778,12 +775,13 @@ func (qs *QuestionService) SearchUserTopList(ctx context.Context, userName strin
|
|||
if !Exist {
|
||||
return userQuestionlist, userAnswerlist, nil
|
||||
}
|
||||
search := &schema.QuestionSearch{}
|
||||
search.Order = "score"
|
||||
search := &schema.QuestionPageReq{}
|
||||
search.OrderCond = "score"
|
||||
search.Page = 0
|
||||
search.PageSize = 5
|
||||
search.UserID = userinfo.ID
|
||||
questionlist, _, err := qs.SearchList(ctx, search, loginUserID)
|
||||
search.UserIDBeSearched = userinfo.ID
|
||||
search.LoginUserID = loginUserID
|
||||
questionlist, _, err := qs.GetQuestionPage(ctx, search)
|
||||
if err != nil {
|
||||
return userQuestionlist, userAnswerlist, err
|
||||
}
|
||||
|
@ -858,57 +856,64 @@ func (qs *QuestionService) SearchByTitleLike(ctx context.Context, title string,
|
|||
}
|
||||
|
||||
// SimilarQuestion
|
||||
func (qs *QuestionService) SimilarQuestion(ctx context.Context, questionID string, loginUserID string) ([]*schema.QuestionInfo, int64, error) {
|
||||
list := make([]*schema.QuestionInfo, 0)
|
||||
func (qs *QuestionService) SimilarQuestion(ctx context.Context, questionID string, loginUserID string) ([]*schema.QuestionPageResp, int64, error) {
|
||||
question, err := qs.questioncommon.Info(ctx, questionID, loginUserID)
|
||||
if err != nil {
|
||||
return list, 0, nil
|
||||
return nil, 0, nil
|
||||
}
|
||||
tagNames := make([]string, 0, len(question.Tags))
|
||||
for _, tag := range question.Tags {
|
||||
tagNames = append(tagNames, tag.SlugName)
|
||||
}
|
||||
search := &schema.QuestionSearch{}
|
||||
search.Order = "frequent"
|
||||
search := &schema.QuestionPageReq{}
|
||||
search.OrderCond = "frequent"
|
||||
search.Page = 0
|
||||
search.PageSize = 6
|
||||
if len(tagNames) > 0 {
|
||||
search.Tag = tagNames[0]
|
||||
}
|
||||
return qs.SearchList(ctx, search, loginUserID)
|
||||
search.LoginUserID = loginUserID
|
||||
return qs.GetQuestionPage(ctx, search)
|
||||
}
|
||||
|
||||
// SearchList
|
||||
func (qs *QuestionService) SearchList(ctx context.Context, req *schema.QuestionSearch, loginUserID string) ([]*schema.QuestionInfo, int64, error) {
|
||||
// GetQuestionPage query questions page
|
||||
func (qs *QuestionService) GetQuestionPage(ctx context.Context, req *schema.QuestionPageReq) (
|
||||
questions []*schema.QuestionPageResp, total int64, err error) {
|
||||
questions = make([]*schema.QuestionPageResp, 0)
|
||||
|
||||
// query by tag condition
|
||||
if len(req.Tag) > 0 {
|
||||
tagInfo, has, err := qs.tagCommon.GetTagBySlugName(ctx, strings.ToLower(req.Tag))
|
||||
tagInfo, exist, err := qs.tagCommon.GetTagBySlugName(ctx, strings.ToLower(req.Tag))
|
||||
if err != nil {
|
||||
log.Error("tagCommon.GetTagListByNames error", err)
|
||||
return nil, 0, err
|
||||
}
|
||||
if has {
|
||||
req.TagIDs = append(req.TagIDs, tagInfo.ID)
|
||||
if exist {
|
||||
req.TagID = tagInfo.ID
|
||||
}
|
||||
}
|
||||
list := make([]*schema.QuestionInfo, 0)
|
||||
if req.UserName != "" {
|
||||
userinfo, exist, err := qs.userCommon.GetUserBasicInfoByUserName(ctx, req.UserName)
|
||||
|
||||
// query by user condition
|
||||
if req.Username != "" {
|
||||
userinfo, exist, err := qs.userCommon.GetUserBasicInfoByUserName(ctx, req.Username)
|
||||
if err != nil {
|
||||
return list, 0, err
|
||||
return nil, 0, err
|
||||
}
|
||||
if !exist {
|
||||
return list, 0, err
|
||||
return questions, 0, nil
|
||||
}
|
||||
req.UserID = userinfo.ID
|
||||
req.UserIDBeSearched = userinfo.ID
|
||||
}
|
||||
questionList, count, err := qs.questionRepo.SearchList(ctx, req)
|
||||
|
||||
questionList, total, err := qs.questionRepo.GetQuestionPage(ctx, req.Page, req.PageSize,
|
||||
req.UserIDBeSearched, req.TagID, req.OrderCond)
|
||||
if err != nil {
|
||||
return list, count, err
|
||||
return nil, 0, err
|
||||
}
|
||||
list, err = qs.questioncommon.ListFormat(ctx, questionList, loginUserID)
|
||||
questions, err = qs.questioncommon.FormatQuestionsPage(ctx, questionList, req.LoginUserID, req.OrderCond)
|
||||
if err != nil {
|
||||
return list, count, err
|
||||
return nil, 0, err
|
||||
}
|
||||
return list, count, nil
|
||||
return questions, total, nil
|
||||
}
|
||||
|
||||
func (qs *QuestionService) AdminSetQuestionStatus(ctx context.Context, questionID string, setStatusStr string) error {
|
||||
|
|
|
@ -27,19 +27,19 @@ func NewSearchParser(tagCommonService *tag_common.TagCommonService, userCommon *
|
|||
// but if match two type, it will return false
|
||||
func (sp *SearchParser) ParseStructure(dto *schema.SearchDTO) (
|
||||
searchType string,
|
||||
// search all
|
||||
// search all
|
||||
userID string,
|
||||
votes int,
|
||||
// search questions
|
||||
// search questions
|
||||
notAccepted bool,
|
||||
isQuestion bool,
|
||||
views,
|
||||
answers int,
|
||||
// search answers
|
||||
// search answers
|
||||
accepted bool,
|
||||
questionID string,
|
||||
isAnswer bool,
|
||||
// common fields
|
||||
// common fields
|
||||
tags,
|
||||
words []string,
|
||||
) {
|
||||
|
@ -202,7 +202,7 @@ func (sp *SearchParser) parseUserID(query *string, currentUserID string) (userID
|
|||
|
||||
re := regexp.MustCompile(exprUserID)
|
||||
res := re.FindStringSubmatch(q)
|
||||
if strings.Index(q, exprMe) != -1 {
|
||||
if strings.Contains(q, exprMe) {
|
||||
userID = currentUserID
|
||||
q = strings.ReplaceAll(q, exprMe, "")
|
||||
} else if len(res) == 2 {
|
||||
|
|
|
@ -236,7 +236,7 @@ func (s *SiteInfoService) UpdateSMTPConfig(ctx context.Context, req *schema.Upda
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
go s.emailService.Send(ctx, req.TestEmailRecipient, title, body, "", "")
|
||||
go s.emailService.SendAndSaveCode(ctx, req.TestEmailRecipient, title, body, "", "")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
|
|
@ -330,28 +330,31 @@ func (ts *TagCommonService) tagFormatRecommendAndReserved(ctx context.Context, t
|
|||
// BatchGetObjectTag batch get object tag
|
||||
func (ts *TagCommonService) BatchGetObjectTag(ctx context.Context, objectIds []string) (map[string][]*schema.TagResp, error) {
|
||||
objectIDTagMap := make(map[string][]*schema.TagResp)
|
||||
tagIDList := make([]string, 0)
|
||||
tagsInfoMap := make(map[string]*entity.Tag)
|
||||
|
||||
tagList, err := ts.tagRelRepo.BatchGetObjectTagRelList(ctx, objectIds)
|
||||
objectTagRelList, err := ts.tagRelRepo.BatchGetObjectTagRelList(ctx, objectIds)
|
||||
if err != nil {
|
||||
return objectIDTagMap, err
|
||||
}
|
||||
for _, tag := range tagList {
|
||||
tagIDList := make([]string, 0)
|
||||
for _, tag := range objectTagRelList {
|
||||
tagIDList = append(tagIDList, tag.TagID)
|
||||
}
|
||||
tagsInfoList, err := ts.GetTagListByIDs(ctx, tagIDList)
|
||||
if err != nil {
|
||||
return objectIDTagMap, err
|
||||
}
|
||||
for _, item := range tagsInfoList {
|
||||
tagsInfoMap[item.ID] = item
|
||||
tagsInfoMapping := make(map[string]*entity.Tag)
|
||||
tagsRank := make(map[string]int) // Used for sorting
|
||||
for idx, item := range tagsInfoList {
|
||||
tagsInfoMapping[item.ID] = item
|
||||
tagsRank[item.ID] = idx
|
||||
}
|
||||
for _, item := range tagList {
|
||||
_, ok := tagsInfoMap[item.TagID]
|
||||
|
||||
for _, item := range objectTagRelList {
|
||||
_, ok := tagsInfoMapping[item.TagID]
|
||||
if ok {
|
||||
tagInfo := tagsInfoMap[item.TagID]
|
||||
tagInfo := tagsInfoMapping[item.TagID]
|
||||
t := &schema.TagResp{
|
||||
ID: tagInfo.ID,
|
||||
SlugName: tagInfo.SlugName,
|
||||
DisplayName: tagInfo.DisplayName,
|
||||
MainTagSlugName: tagInfo.MainTagSlugName,
|
||||
|
@ -361,12 +364,10 @@ func (ts *TagCommonService) BatchGetObjectTag(ctx context.Context, objectIds []s
|
|||
objectIDTagMap[item.ObjectID] = append(objectIDTagMap[item.ObjectID], t)
|
||||
}
|
||||
}
|
||||
for _, taglist := range objectIDTagMap {
|
||||
sort.SliceStable(taglist, func(i, j int) bool {
|
||||
return taglist[i].Reserved
|
||||
})
|
||||
sort.SliceStable(taglist, func(i, j int) bool {
|
||||
return taglist[i].Recommend
|
||||
// The sorting in tagsRank is correct, object tags should be sorted by tagsRank
|
||||
for _, objectTags := range objectIDTagMap {
|
||||
sort.SliceStable(objectTags, func(i, j int) bool {
|
||||
return tagsRank[objectTags[i].ID] < tagsRank[objectTags[j].ID]
|
||||
})
|
||||
}
|
||||
return objectIDTagMap, nil
|
||||
|
|
|
@ -126,6 +126,10 @@ func (us *UserCommon) MakeUsername(ctx context.Context, displayName string) (use
|
|||
return "", errors.BadRequest(reason.UsernameInvalid)
|
||||
}
|
||||
|
||||
if checker.IsReservedUsername(username) {
|
||||
return "", errors.BadRequest(reason.UsernameInvalid)
|
||||
}
|
||||
|
||||
for {
|
||||
_, has, err := us.userRepo.GetByUsername(ctx, username+suffix)
|
||||
if err != nil {
|
||||
|
|
|
@ -20,6 +20,7 @@ import (
|
|||
"github.com/answerdev/answer/internal/service/service_config"
|
||||
"github.com/answerdev/answer/internal/service/siteinfo_common"
|
||||
usercommon "github.com/answerdev/answer/internal/service/user_common"
|
||||
"github.com/answerdev/answer/pkg/checker"
|
||||
"github.com/google/uuid"
|
||||
"github.com/segmentfault/pacman/errors"
|
||||
"github.com/segmentfault/pacman/log"
|
||||
|
@ -168,7 +169,7 @@ func (us *UserService) RetrievePassWord(ctx context.Context, req *schema.UserRet
|
|||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
go us.emailService.Send(ctx, req.Email, title, body, code, data.ToJSONString())
|
||||
go us.emailService.SendAndSaveCode(ctx, req.Email, title, body, code, data.ToJSONString())
|
||||
return code, nil
|
||||
}
|
||||
|
||||
|
@ -240,20 +241,31 @@ func (us *UserService) UserModifyPassword(ctx context.Context, request *schema.U
|
|||
}
|
||||
|
||||
// UpdateInfo update user info
|
||||
func (us *UserService) UpdateInfo(ctx context.Context, req *schema.UpdateInfoRequest) (err error) {
|
||||
func (us *UserService) UpdateInfo(ctx context.Context, req *schema.UpdateInfoRequest) (
|
||||
errFields []*validator.FormErrorField, err error) {
|
||||
if len(req.Username) > 0 {
|
||||
userInfo, exist, err := us.userRepo.GetByUsername(ctx, req.Username)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
if exist && userInfo.ID != req.UserID {
|
||||
return errors.BadRequest(reason.UsernameDuplicate)
|
||||
errFields = append(errFields, &validator.FormErrorField{
|
||||
ErrorField: "username",
|
||||
ErrorMsg: reason.UsernameDuplicate,
|
||||
})
|
||||
return errFields, errors.BadRequest(reason.UsernameDuplicate)
|
||||
}
|
||||
if checker.IsReservedUsername(req.Username) {
|
||||
errFields = append(errFields, &validator.FormErrorField{
|
||||
ErrorField: "username",
|
||||
ErrorMsg: reason.UsernameInvalid,
|
||||
})
|
||||
return errFields, errors.BadRequest(reason.UsernameInvalid)
|
||||
}
|
||||
}
|
||||
avatar, err := json.Marshal(req.Avatar)
|
||||
if err != nil {
|
||||
err = errors.BadRequest(reason.UserSetAvatar).WithError(err).WithStack()
|
||||
return err
|
||||
return nil, errors.BadRequest(reason.UserSetAvatar).WithError(err).WithStack()
|
||||
}
|
||||
userInfo := entity.User{}
|
||||
userInfo.ID = req.UserID
|
||||
|
@ -264,10 +276,8 @@ func (us *UserService) UpdateInfo(ctx context.Context, req *schema.UpdateInfoReq
|
|||
userInfo.Location = req.Location
|
||||
userInfo.Website = req.Website
|
||||
userInfo.Username = req.Username
|
||||
if err := us.userRepo.UpdateInfo(ctx, &userInfo); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
err = us.userRepo.UpdateInfo(ctx, &userInfo)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
func (us *UserService) UserEmailHas(ctx context.Context, email string) (bool, error) {
|
||||
|
@ -292,14 +302,18 @@ func (us *UserService) UserUpdateInterface(ctx context.Context, req *schema.Upda
|
|||
|
||||
// UserRegisterByEmail user register
|
||||
func (us *UserService) UserRegisterByEmail(ctx context.Context, registerUserInfo *schema.UserRegisterReq) (
|
||||
resp *schema.GetUserResp, err error,
|
||||
resp *schema.GetUserResp, errFields []*validator.FormErrorField, err error,
|
||||
) {
|
||||
_, has, err := us.userRepo.GetByEmail(ctx, registerUserInfo.Email)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
if has {
|
||||
return nil, errors.BadRequest(reason.EmailDuplicate)
|
||||
errFields = append(errFields, &validator.FormErrorField{
|
||||
ErrorField: "e_mail",
|
||||
ErrorMsg: reason.EmailDuplicate,
|
||||
})
|
||||
return nil, errFields, errors.BadRequest(reason.EmailDuplicate)
|
||||
}
|
||||
|
||||
userInfo := &entity.User{}
|
||||
|
@ -307,11 +321,15 @@ func (us *UserService) UserRegisterByEmail(ctx context.Context, registerUserInfo
|
|||
userInfo.DisplayName = registerUserInfo.Name
|
||||
userInfo.Pass, err = us.encryptPassword(ctx, registerUserInfo.Pass)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
userInfo.Username, err = us.userCommonService.MakeUsername(ctx, registerUserInfo.Name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
errFields = append(errFields, &validator.FormErrorField{
|
||||
ErrorField: "name",
|
||||
ErrorMsg: reason.UsernameInvalid,
|
||||
})
|
||||
return nil, errFields, err
|
||||
}
|
||||
userInfo.IPInfo = registerUserInfo.IP
|
||||
userInfo.MailStatus = entity.EmailStatusToBeVerified
|
||||
|
@ -319,7 +337,7 @@ func (us *UserService) UserRegisterByEmail(ctx context.Context, registerUserInfo
|
|||
userInfo.LastLoginDate = time.Now()
|
||||
err = us.userRepo.AddUser(ctx, userInfo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// send email
|
||||
|
@ -331,9 +349,9 @@ func (us *UserService) UserRegisterByEmail(ctx context.Context, registerUserInfo
|
|||
verifyEmailURL := fmt.Sprintf("%s/users/account-activation?code=%s", us.getSiteUrl(ctx), code)
|
||||
title, body, err := us.emailService.RegisterTemplate(ctx, verifyEmailURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
go us.emailService.Send(ctx, userInfo.EMail, title, body, code, data.ToJSONString())
|
||||
go us.emailService.SendAndSaveCode(ctx, userInfo.EMail, title, body, code, data.ToJSONString())
|
||||
|
||||
roleID, err := us.userRoleService.GetUserRole(ctx, userInfo.ID)
|
||||
if err != nil {
|
||||
|
@ -351,16 +369,16 @@ func (us *UserService) UserRegisterByEmail(ctx context.Context, registerUserInfo
|
|||
}
|
||||
resp.AccessToken, err = us.authService.SetUserCacheInfo(ctx, userCacheInfo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
resp.IsAdmin = userCacheInfo.IsAdmin
|
||||
if resp.IsAdmin {
|
||||
err = us.authService.SetAdminUserCacheInfo(ctx, resp.AccessToken, &entity.UserCacheInfo{UserID: userInfo.ID})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
return resp, nil
|
||||
return resp, nil, nil
|
||||
}
|
||||
|
||||
func (us *UserService) UserVerifyEmailSend(ctx context.Context, userID string) error {
|
||||
|
@ -382,7 +400,7 @@ func (us *UserService) UserVerifyEmailSend(ctx context.Context, userID string) e
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
go us.emailService.Send(ctx, userInfo.EMail, title, body, code, data.ToJSONString())
|
||||
go us.emailService.SendAndSaveCode(ctx, userInfo.EMail, title, body, code, data.ToJSONString())
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -514,7 +532,7 @@ func (us *UserService) UserChangeEmailSendCode(ctx context.Context, req *schema.
|
|||
}
|
||||
log.Infof("send email confirmation %s", verifyEmailURL)
|
||||
|
||||
go us.emailService.Send(context.Background(), req.Email, title, body, code, data.ToJSONString())
|
||||
go us.emailService.SendAndSaveCode(context.Background(), req.Email, title, body, code, data.ToJSONString())
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
|
@ -598,6 +616,25 @@ func (us *UserService) UserRanking(ctx context.Context) (resp *schema.UserRankin
|
|||
return us.warpStatRankingResp(userInfoMapping, rankStat, voteStat, userRoleRels), nil
|
||||
}
|
||||
|
||||
// UserUnsubscribeEmailNotification user unsubscribe email notification
|
||||
func (us *UserService) UserUnsubscribeEmailNotification(
|
||||
ctx context.Context, req *schema.UserUnsubscribeEmailNotificationReq) (err error) {
|
||||
data := &schema.EmailCodeContent{}
|
||||
err = data.FromJSONString(req.Content)
|
||||
if err != nil || len(data.UserID) == 0 {
|
||||
return errors.BadRequest(reason.EmailVerifyURLExpired)
|
||||
}
|
||||
|
||||
userInfo, exist, err := us.userRepo.GetByUserID(ctx, data.UserID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !exist {
|
||||
return errors.BadRequest(reason.UserNotFound)
|
||||
}
|
||||
return us.userRepo.UpdateNoticeStatus(ctx, userInfo.ID, schema.NoticeStatusOff)
|
||||
}
|
||||
|
||||
func (us *UserService) getActivityUserRankStat(ctx context.Context, startTime, endTime time.Time, limit int,
|
||||
userIDExist map[string]bool) (rankStat []*entity.ActivityUserRankStat, userIDs []string, err error) {
|
||||
rankStat, err = us.activityRepo.GetUsersWhoHasGainedTheMostReputation(ctx, startTime, endTime, limit)
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
package checker
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
|
||||
"github.com/answerdev/answer/configs"
|
||||
"github.com/answerdev/answer/internal/cli"
|
||||
"github.com/answerdev/answer/pkg/dir"
|
||||
)
|
||||
|
||||
var (
|
||||
reservedUsernameMapping = make(map[string]bool)
|
||||
reservedUsernameInit sync.Once
|
||||
)
|
||||
|
||||
func initReservedUsername() {
|
||||
reservedUsernamesJsonFilePath := filepath.Join(cli.ConfigFileDir, cli.DefaultReservedUsernamesConfigFileName)
|
||||
if dir.CheckFileExist(reservedUsernamesJsonFilePath) {
|
||||
// if reserved username file exists, read it and replace configuration
|
||||
reservedUsernamesJsonFile, err := os.ReadFile(reservedUsernamesJsonFilePath)
|
||||
if err == nil {
|
||||
configs.ReservedUsernames = reservedUsernamesJsonFile
|
||||
}
|
||||
}
|
||||
var usernames []string
|
||||
_ = json.Unmarshal(configs.ReservedUsernames, &usernames)
|
||||
for _, username := range usernames {
|
||||
reservedUsernameMapping[username] = true
|
||||
}
|
||||
}
|
||||
|
||||
// IsReservedUsername checks whether the username is reserved
|
||||
func IsReservedUsername(username string) bool {
|
||||
reservedUsernameInit.Do(initReservedUsername)
|
||||
return reservedUsernameMapping[username]
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
package checker
|
||||
|
||||
// IsNotZeroString check s is not empty string and is not "0"
|
||||
func IsNotZeroString(s string) bool {
|
||||
return len(s) > 0 && s != "0"
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
package encryption
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
)
|
||||
|
||||
// MD5 return md5 hash
|
||||
func MD5(data string) string {
|
||||
h := md5.New()
|
||||
h.Write([]byte(data))
|
||||
return hex.EncodeToString(h.Sum(nil))
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
package htmltext
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
|
@ -75,11 +75,10 @@ func FetchExcerpt(html, trimMarker string, limit int) (text string) {
|
|||
runeText := []rune(text)
|
||||
if len(runeText) <= limit {
|
||||
text = string(runeText)
|
||||
} else {
|
||||
text = string(runeText[0:limit])
|
||||
return
|
||||
}
|
||||
|
||||
text += trimMarker
|
||||
text = string(runeText[0:limit]) + trimMarker
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -89,7 +88,7 @@ func GetPicByUrl(Url string) string {
|
|||
return ""
|
||||
}
|
||||
defer res.Body.Close()
|
||||
pix, err := ioutil.ReadAll(res.Body)
|
||||
pix, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
|
|
@ -50,6 +50,10 @@ func TestFetchExcerpt(t *testing.T) {
|
|||
expected = "hello你好😂..."
|
||||
text = FetchExcerpt("<p>hello你好😂world</p>", "...", 8)
|
||||
assert.Equal(t, expected, text)
|
||||
|
||||
expected = "hello你好"
|
||||
text = FetchExcerpt("<p>hello你好</p>", "...", 8)
|
||||
assert.Equal(t, expected, text)
|
||||
}
|
||||
|
||||
func TestUrlTitle(t *testing.T) {
|
||||
|
|
|
@ -1 +1 @@
|
|||
<!doctype html><html><head><meta charset="utf-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><link rel="manifest" href="/manifest.json"/><script defer="defer" src="/static/js/main.554b9f62.js"></script><link href="/static/css/main.401dc3ca.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"><div id="spin-mask" hidden><script>try{document.querySelector("#spin-mask").removeAttribute("hidden")}catch(e){}</script><style>@keyframes _doc-spin{to{transform:rotate(360deg)}}#spin-mask{position:fixed;top:0;right:0;bottom:0;left:0;background-color:#fff;z-index:9999}#spin-container{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)}#spin-container .spinner{box-sizing:border-box;display:inline-block;width:2rem;height:2rem;vertical-align:-.125em;border:.25rem solid currentColor;border-right-color:transparent;color:rgba(108,117,125,.75);border-radius:50%;animation:.75s linear infinite _doc-spin}</style><div id="spin-container"><div class="spinner"></div></div></div></div></body></html>
|
||||
<!doctype html><html><head><meta charset="utf-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><link rel="manifest" href="/manifest.json"/><script defer="defer" src="/static/js/main.fde484b3.js"></script><link href="/static/css/main.401dc3ca.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"><div id="spin-mask"><noscript><style>#spin-mask{display:none!important}</style></noscript><style>@keyframes _doc-spin{to{transform:rotate(360deg)}}#spin-mask{position:fixed;top:0;right:0;bottom:0;left:0;background-color:#fff;z-index:9999}#spin-container{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)}#spin-container .spinner{box-sizing:border-box;display:inline-block;width:2rem;height:2rem;vertical-align:-.125em;border:.25rem solid currentColor;border-right-color:transparent;color:rgba(108,117,125,.75);border-radius:50%;animation:.75s linear infinite _doc-spin}</style><div id="spin-container"><div class="spinner"></div></div></div></div></body></html>
|
1131
ui/pnpm-lock.yaml
1131
ui/pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
|
@ -258,7 +258,7 @@ const Comment = ({ objectId, mode, commentId }) => {
|
|||
)}
|
||||
|
||||
<div
|
||||
className="fmt fs-14"
|
||||
className="fmt fs-14 text-break text-wrap"
|
||||
dangerouslySetInnerHTML={{ __html: item.parsed_text }}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -93,16 +93,18 @@ const Index: FC<Props> = ({
|
|||
className={classnames(
|
||||
'fs-14 font-monospace',
|
||||
newData.original_text && 'mb-4',
|
||||
)}>
|
||||
{`/tags/${
|
||||
newData?.main_tag_slug_name
|
||||
? diffText(
|
||||
newData.main_tag_slug_name,
|
||||
oldData?.main_tag_slug_name,
|
||||
)
|
||||
: diffText(newData.slug_name, oldData?.slug_name)
|
||||
}`}
|
||||
</div>
|
||||
)}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `/tags/${
|
||||
newData?.main_tag_slug_name
|
||||
? diffText(
|
||||
newData.main_tag_slug_name,
|
||||
oldData?.main_tag_slug_name,
|
||||
)
|
||||
: diffText(newData.slug_name, oldData?.slug_name)
|
||||
}`,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
dangerouslySetInnerHTML={{
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { FC, memo } from 'react';
|
||||
import { Nav, Dropdown } from 'react-bootstrap';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Link, NavLink } from 'react-router-dom';
|
||||
import { NavLink, useNavigate } from 'react-router-dom';
|
||||
|
||||
import type * as Type from '@/common/interface';
|
||||
import { Avatar, Icon } from '@/components';
|
||||
|
@ -14,6 +14,13 @@ interface Props {
|
|||
|
||||
const Index: FC<Props> = ({ redDot, userInfo, logOut }) => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const handleLinkClick = (evt) => {
|
||||
evt.preventDefault();
|
||||
const { href } = evt.currentTarget;
|
||||
const { pathname } = new URL(href);
|
||||
navigate(pathname);
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<Nav className="flex-row">
|
||||
|
@ -26,7 +33,7 @@ const Index: FC<Props> = ({ redDot, userInfo, logOut }) => {
|
|||
</Nav.Link>
|
||||
|
||||
<Nav.Link
|
||||
as={Link}
|
||||
as={NavLink}
|
||||
to="/users/notifications/achievement"
|
||||
className="icon-link d-flex align-items-center justify-content-center p-0 me-3 position-relative">
|
||||
<Icon name="trophy-fill" className="fs-4" />
|
||||
|
@ -46,17 +53,26 @@ const Index: FC<Props> = ({ redDot, userInfo, logOut }) => {
|
|||
</Dropdown.Toggle>
|
||||
|
||||
<Dropdown.Menu>
|
||||
<Dropdown.Item href={`/users/${userInfo.username}`}>
|
||||
<Dropdown.Item
|
||||
href={`/users/${userInfo.username}`}
|
||||
onClick={handleLinkClick}>
|
||||
{t('header.nav.profile')}
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item href="/users/settings/profile">
|
||||
<Dropdown.Item
|
||||
href="/users/settings/profile"
|
||||
onClick={handleLinkClick}>
|
||||
{t('header.nav.setting')}
|
||||
</Dropdown.Item>
|
||||
{userInfo?.is_admin ? (
|
||||
<Dropdown.Item href="/admin">{t('header.nav.admin')}</Dropdown.Item>
|
||||
<Dropdown.Item href="/admin" onClick={handleLinkClick}>
|
||||
{t('header.nav.admin')}
|
||||
</Dropdown.Item>
|
||||
) : null}
|
||||
{redDot?.can_revision ? (
|
||||
<Dropdown.Item href="/review" className="position-relative">
|
||||
<Dropdown.Item
|
||||
href="/review"
|
||||
className="position-relative"
|
||||
onClick={handleLinkClick}>
|
||||
{t('header.nav.review')}
|
||||
{redDot?.revision > 0 && (
|
||||
<span className="position-absolute top-50 translate-middle-y end-0 me-3 p-2 bg-danger border border-light rounded-circle">
|
||||
|
|
|
@ -49,6 +49,14 @@ const Header: FC = () => {
|
|||
const handleInput = (val) => {
|
||||
setSearch(val);
|
||||
};
|
||||
const handleSearch = (evt) => {
|
||||
evt.preventDefault();
|
||||
if (!searchStr) {
|
||||
return;
|
||||
}
|
||||
const searchUrl = `/search?q=${encodeURIComponent(searchStr)}`;
|
||||
navigate(searchUrl);
|
||||
};
|
||||
|
||||
const handleLogout = async () => {
|
||||
await logout();
|
||||
|
@ -118,7 +126,13 @@ const Header: FC = () => {
|
|||
<NavItems redDot={redDot} userInfo={user} logOut={handleLogout} />
|
||||
) : (
|
||||
<>
|
||||
<Button variant="link" className="me-2" href="/users/login">
|
||||
<Button
|
||||
variant="link"
|
||||
className={classnames('me-2', {
|
||||
'link-light': navbarStyle === 'theme-colored',
|
||||
'link-primary': navbarStyle !== 'theme-colored',
|
||||
})}
|
||||
href="/users/login">
|
||||
{t('btns.login')}
|
||||
</Button>
|
||||
{loginSetting.allow_new_registrations && (
|
||||
|
@ -153,7 +167,10 @@ const Header: FC = () => {
|
|||
<hr className="hr lg-none mt-2" />
|
||||
|
||||
<Col lg={4} className="d-flex justify-content-center">
|
||||
<Form action="/search" className="w-75 px-0 px-lg-2">
|
||||
<Form
|
||||
action="/search"
|
||||
className="w-75 px-0 px-lg-2"
|
||||
onSubmit={handleSearch}>
|
||||
<FormControl
|
||||
placeholder={t('header.search.placeholder')}
|
||||
className="placeholder-search"
|
||||
|
|
|
@ -28,60 +28,6 @@ interface Props {
|
|||
source: 'questions' | 'tag';
|
||||
}
|
||||
|
||||
const QuestionLastUpdate = ({ q }) => {
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'question' });
|
||||
if (q.update_time > q.edit_time) {
|
||||
// question answered
|
||||
return (
|
||||
<div className="d-flex">
|
||||
<BaseUserCard
|
||||
data={q.last_answered_user_info}
|
||||
showAvatar={false}
|
||||
className="me-1"
|
||||
/>
|
||||
•
|
||||
<FormatTime
|
||||
time={q.update_time}
|
||||
className="text-secondary ms-1"
|
||||
preFix={t('answered')}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (q.edit_time > q.update_time) {
|
||||
// question modified
|
||||
return (
|
||||
<div className="d-flex">
|
||||
<BaseUserCard
|
||||
data={q.update_user_info}
|
||||
showAvatar={false}
|
||||
className="me-1"
|
||||
/>
|
||||
•
|
||||
<FormatTime
|
||||
time={q.edit_time}
|
||||
className="text-secondary ms-1"
|
||||
preFix={t('modified')}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// default: asked
|
||||
return (
|
||||
<div className="d-flex">
|
||||
<BaseUserCard data={q.user_info} showAvatar={false} className="me-1" />
|
||||
•
|
||||
<FormatTime
|
||||
time={q.create_time}
|
||||
preFix={t('asked')}
|
||||
className="text-secondary ms-1"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const QuestionList: FC<Props> = ({ source }) => {
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'question' });
|
||||
const { tagName = '' } = useParams();
|
||||
|
@ -132,7 +78,19 @@ const QuestionList: FC<Props> = ({ source }) => {
|
|||
</NavLink>
|
||||
</h5>
|
||||
<div className="d-flex flex-column flex-md-row align-items-md-center fs-14 mb-2 text-secondary">
|
||||
<QuestionLastUpdate q={li} />
|
||||
<div className="d-flex">
|
||||
<BaseUserCard
|
||||
data={li.operator}
|
||||
showAvatar={false}
|
||||
className="me-1"
|
||||
/>
|
||||
•
|
||||
<FormatTime
|
||||
time={li.operated_at}
|
||||
className="text-secondary ms-1"
|
||||
preFix={t(li.operation_type)}
|
||||
/>
|
||||
</div>
|
||||
<div className="ms-0 ms-md-3 mt-2 mt-md-0">
|
||||
<span>
|
||||
<Icon name="hand-thumbs-up-fill" />
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import React, { memo, FC } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
|
@ -21,8 +22,8 @@ const Index: FC<IProps> = ({
|
|||
href ||= pathFactory.tagLanding(data?.slug_name);
|
||||
|
||||
return (
|
||||
<a
|
||||
href={href}
|
||||
<Link
|
||||
to={href}
|
||||
className={classNames(
|
||||
'badge-tag rounded-1',
|
||||
data.reserved && 'badge-tag-reserved',
|
||||
|
@ -30,7 +31,7 @@ const Index: FC<IProps> = ({
|
|||
className,
|
||||
)}>
|
||||
<span className={textClassName}>{data.slug_name}</span>
|
||||
</a>
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -253,7 +253,7 @@ const TagSelector: FC<IProps> = ({
|
|||
variant="link"
|
||||
className="px-3 btn-no-border w-100 text-start"
|
||||
onClick={() => {
|
||||
tagModal.onShow();
|
||||
tagModal.onShow(tag);
|
||||
}}>
|
||||
+ {t('create_btn')}
|
||||
</Button>
|
||||
|
|
|
@ -39,8 +39,21 @@ const useTagModal = (props: IProps = {}) => {
|
|||
setVisibleState(false);
|
||||
};
|
||||
|
||||
const onShow = () => {
|
||||
const onShow = (searchStr = '') => {
|
||||
setVisibleState(true);
|
||||
setFormData({
|
||||
...formData,
|
||||
displayName: {
|
||||
value: searchStr,
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
slugName: {
|
||||
value: searchStr,
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const checkValidated = (): boolean => {
|
||||
|
|
|
@ -16,7 +16,7 @@ const AnswerLinks = () => {
|
|||
</Col>
|
||||
<Col xs={6}>
|
||||
<a href="https://meta.answer.dev" target="_blank" rel="noreferrer">
|
||||
{t('feedback')}
|
||||
{t('support')}
|
||||
</a>
|
||||
</Col>
|
||||
</Row>
|
||||
|
|
|
@ -69,7 +69,7 @@ const Index: FC<Props> = ({
|
|||
<div id={data.id} ref={answerRef} className="answer-item py-4">
|
||||
<article
|
||||
dangerouslySetInnerHTML={{ __html: data?.html }}
|
||||
className="fmt"
|
||||
className="fmt text-break text-wrap"
|
||||
/>
|
||||
<div className="d-flex align-items-center mt-4">
|
||||
<Actions
|
||||
|
|
|
@ -26,6 +26,7 @@ const Index: FC<Props> = ({ data }) => {
|
|||
answerId: data.object.id,
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<ListGroupItem className="py-3 px-0 border-start-0 border-end-0 bg-transparent">
|
||||
<div className="mb-2 clearfix">
|
||||
|
|
|
@ -31,13 +31,15 @@ const Index = () => {
|
|||
|
||||
const getProfile = () => {
|
||||
getLoggedUserInfo().then((res) => {
|
||||
setFormData({
|
||||
notice_switch: {
|
||||
value: res.notice_status === 1,
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
});
|
||||
if (res) {
|
||||
setFormData({
|
||||
notice_switch: {
|
||||
value: res.notice_status === 1,
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -43,12 +43,14 @@ const Index: React.FC = () => {
|
|||
});
|
||||
const getProfile = () => {
|
||||
getLoggedUserInfo().then((res) => {
|
||||
formData.display_name.value = res.display_name;
|
||||
formData.bio.value = res.bio;
|
||||
formData.avatar.value = res.avatar;
|
||||
formData.location.value = res.location;
|
||||
formData.website.value = res.website;
|
||||
setFormData({ ...formData });
|
||||
if (res) {
|
||||
formData.display_name.value = res.display_name;
|
||||
formData.bio.value = res.bio;
|
||||
formData.avatar.value = res.avatar;
|
||||
formData.location.value = res.location;
|
||||
formData.website.value = res.website;
|
||||
setFormData({ ...formData });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
import { FC, memo, useEffect } from 'react';
|
||||
import { Container, Row, Col } from 'react-bootstrap';
|
||||
import { Link, useSearchParams } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { unsubscribe } from '@/services';
|
||||
import { usePageTags } from '@/hooks';
|
||||
|
||||
const Index: FC = () => {
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'unsubscribe' });
|
||||
usePageTags({
|
||||
title: t('page_title'),
|
||||
});
|
||||
const [searchParams] = useSearchParams();
|
||||
const code = searchParams.get('code');
|
||||
useEffect(() => {
|
||||
if (code) {
|
||||
unsubscribe(code);
|
||||
}
|
||||
}, [code]);
|
||||
return (
|
||||
<Container className="pt-4 mt-2 mb-5">
|
||||
<Row className="justify-content-center">
|
||||
<Col lg={6}>
|
||||
<h3 className="text-center mt-3 mb-5">{t('success_title')}</h3>
|
||||
<p className="text-center">{t('success_desc')}</p>
|
||||
<div className="text-center">
|
||||
<Link to="/users/settings/notify">{t('link')}</Link>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(Index);
|
|
@ -337,6 +337,10 @@ const routes: RouteNode[] = [
|
|||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/users/unsubscribe',
|
||||
page: 'pages/Users/Unsubscribe',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
|
@ -20,7 +20,7 @@ export const useQuestionList = (params: Type.QueryQuestionsReq) => {
|
|||
export const useHotQuestions = (
|
||||
params: Type.QueryQuestionsReq = {
|
||||
page: 1,
|
||||
page_size: 10,
|
||||
page_size: 6,
|
||||
order: 'frequent',
|
||||
},
|
||||
) => {
|
||||
|
|
|
@ -257,3 +257,8 @@ export const getAppSettings = () => {
|
|||
export const reopenQuestion = (params: { question_id: string }) => {
|
||||
return request.put('/answer/api/v1/question/reopen', params);
|
||||
};
|
||||
|
||||
export const unsubscribe = (code: string) => {
|
||||
const apiUrl = '/answer/api/v1/user/email/notification';
|
||||
return request.put(apiUrl, { code });
|
||||
};
|
||||
|
|
|
@ -81,20 +81,13 @@ function formatUptime(value) {
|
|||
return `< 1 ${t('dates.hour')}`;
|
||||
}
|
||||
|
||||
function escapeRemove(str) {
|
||||
function escapeRemove(str: string) {
|
||||
if (!str || typeof str !== 'string') return str;
|
||||
const arrEntities = {
|
||||
lt: '<',
|
||||
gt: '>',
|
||||
nbsp: ' ',
|
||||
amp: '&',
|
||||
quot: '"',
|
||||
'#39': "'",
|
||||
};
|
||||
|
||||
return str.replace(/&(lt|gt|nbsp|amp|quot|#39);/gi, function (all, t) {
|
||||
return arrEntities[t];
|
||||
});
|
||||
let temp: HTMLDivElement | null = document.createElement('div');
|
||||
temp.innerHTML = str;
|
||||
const output = temp?.innerText || temp.textContent;
|
||||
temp = null;
|
||||
return output;
|
||||
}
|
||||
function mixColor(color_1, color_2, weight) {
|
||||
function d2h(d) {
|
||||
|
@ -179,6 +172,8 @@ function diffText(newText: string, oldText: string): string {
|
|||
?.replace(/<input/gi, '<input');
|
||||
}
|
||||
const diff = Diff.diffChars(oldText, newText);
|
||||
console.log(diff);
|
||||
|
||||
const result = diff.map((part) => {
|
||||
if (part.added) {
|
||||
if (part.value.replace(/\n/g, '').length <= 0) {
|
||||
|
|
|
@ -8,12 +8,23 @@ const differentCurrent = (target: string, base?: string) => {
|
|||
return targetUrl.toString() !== window.location.href;
|
||||
};
|
||||
|
||||
const storageLoginRedirect = () => {
|
||||
const { pathname } = window.location;
|
||||
if (pathname !== RouteAlias.login && pathname !== RouteAlias.register) {
|
||||
const loc = window.location;
|
||||
const redirectUrl = loc.href.replace(loc.origin, '');
|
||||
Storage.set(REDIRECT_PATH_STORAGE_KEY, redirectUrl);
|
||||
}
|
||||
};
|
||||
/**
|
||||
* only navigate if not same as current url
|
||||
* @param pathname
|
||||
* @param callback
|
||||
*/
|
||||
const navigate = (pathname: string, callback: Function) => {
|
||||
if (pathname === RouteAlias.login) {
|
||||
storageLoginRedirect();
|
||||
}
|
||||
if (differentCurrent(pathname)) {
|
||||
callback();
|
||||
}
|
||||
|
@ -23,12 +34,7 @@ const navigate = (pathname: string, callback: Function) => {
|
|||
* auto navigate to login page with redirect info
|
||||
*/
|
||||
const navigateToLogin = () => {
|
||||
const { pathname } = window.location;
|
||||
if (pathname !== RouteAlias.login && pathname !== RouteAlias.register) {
|
||||
const loc = window.location;
|
||||
const redirectUrl = loc.href.replace(loc.origin, '');
|
||||
Storage.set(REDIRECT_PATH_STORAGE_KEY, redirectUrl);
|
||||
}
|
||||
storageLoginRedirect();
|
||||
navigate(RouteAlias.login, () => {
|
||||
window.location.replace(RouteAlias.login);
|
||||
});
|
||||
|
|
|
@ -25,31 +25,21 @@
|
|||
>
|
||||
<div class="d-flex">
|
||||
<div class="text-secondary me-1">
|
||||
<a href="/users/{{.UserInfo.Username}}"
|
||||
<a href="/users/{{.Operator.Username}}"
|
||||
><span class="me-1 text-break"
|
||||
>{{.UserInfo.DisplayName}}</span
|
||||
>{{.Operator.DisplayName}}</span
|
||||
></a
|
||||
><span class="fw-bold" title="Reputation"
|
||||
>{{.UserInfo.Rank}}</span
|
||||
>{{.Operator.Rank}}</span
|
||||
>
|
||||
</div>
|
||||
• {{if eq .CreateTime .UpdateTime}}
|
||||
<time
|
||||
class="text-secondary ms-1"
|
||||
datetime="{{timeFormatISO $.timezone .CreateTime}}"
|
||||
title="{{translatorTimeFormatLongDate $.language $.timezone .CreateTime}}"
|
||||
datetime="{{timeFormatISO $.timezone .OperatedAt}}"
|
||||
title="{{translatorTimeFormatLongDate $.language $.timezone .OperatedAt}}"
|
||||
>{{translator $.language "ui.question.asked"}}
|
||||
{{translatorTimeFormat $.language $.timezone .CreateTime}}
|
||||
{{translatorTimeFormat $.language $.timezone .OperatedAt}}
|
||||
</time>
|
||||
{{else if gt .UpdateTime 0}}
|
||||
<time
|
||||
class="text-secondary ms-1"
|
||||
datetime="{{timeFormatISO $.timezone .UpdateTime}}"
|
||||
title="{{translatorTimeFormatLongDate $.language $.timezone .UpdateTime}}"
|
||||
>{{translator $.language "ui.question.modified"}}
|
||||
{{translatorTimeFormat $.language $.timezone .UpdateTime}}
|
||||
</time>
|
||||
{{end}}
|
||||
</div>
|
||||
<div class="ms-0 ms-md-3 mt-2 mt-md-0">
|
||||
<span
|
||||
|
|
|
@ -34,21 +34,21 @@
|
|||
>
|
||||
<div class="d-flex">
|
||||
<div class="text-secondary me-1">
|
||||
<a href="/users/{{.UserInfo.Username}}"
|
||||
<a href="/users/{{.Operator.Username}}"
|
||||
><span class="me-1 text-break"
|
||||
>{{.UserInfo.DisplayName}}</span
|
||||
>{{.Operator.DisplayName}}</span
|
||||
></a
|
||||
><span class="fw-bold" title="Reputation"
|
||||
>{{.UserInfo.Rank}}</span
|
||||
>{{.Operator.Rank}}</span
|
||||
>
|
||||
</div>
|
||||
•
|
||||
<time
|
||||
class="text-secondary ms-1"
|
||||
datetime="{{timeFormatISO $.timezone .CreateTime}}"
|
||||
title="{{translatorTimeFormatLongDate $.language $.timezone .CreateTime}}"
|
||||
datetime="{{timeFormatISO $.timezone .OperatedAt}}"
|
||||
title="{{translatorTimeFormatLongDate $.language $.timezone .OperatedAt}}"
|
||||
>{{translator $.language "ui.question.asked"}}
|
||||
{{translatorTimeFormat $.language $.timezone .CreateTime}}
|
||||
{{translatorTimeFormat $.language $.timezone .OperatedAt}}
|
||||
</time>
|
||||
</div>
|
||||
<div class="ms-0 ms-md-3 mt-2 mt-md-0">
|
||||
|
|
Loading…
Reference in New Issue