mirror of https://gitee.com/answerdev/answer.git
Merge branch 'feat/1.1.0/report' into feat/1.1.0/context
# Conflicts: # internal/repo/rank/user_rank_repo.go # internal/repo/tag/tag_rel_repo.go # internal/service/report_admin/report_backyard.go
This commit is contained in:
commit
f16207e30f
|
@ -15,7 +15,7 @@ builds:
|
|||
- id: build
|
||||
main: ./cmd/answer/.
|
||||
binary: answer
|
||||
ldflags: -s -w -X github.com/answerdev/answer/cmd.Version={{.Version}} -X github.com/answerdev/answer/cmd.Revision={{.ShortCommit}} -X github.com/answerdev/answer/cmd.Time={{.Date}} -X main.BuildUser=goreleaser
|
||||
ldflags: -s -w -X main.Version={{.Version}} -X main.Revision={{.ShortCommit}} -X main.Time={{.Date}} -X main.BuildUser=goreleaser
|
||||
flags: -v
|
||||
goos:
|
||||
- linux
|
||||
|
@ -26,7 +26,7 @@ builds:
|
|||
- id: build-windows
|
||||
main: ./cmd/answer/.
|
||||
binary: answer
|
||||
ldflags: -s -w -X github.com/answerdev/answer/cmd.Version={{.Version}} -X github.com/answerdev/answer/cmd.Revision={{.ShortCommit}} -X github.com/answerdev/answer/cmd.Time={{.Date}} -X main.BuildUser=goreleaser
|
||||
ldflags: -s -w -X main.Version={{.Version}} -X main.Revision={{.ShortCommit}} -X main.Time={{.Date}} -X main.BuildUser=goreleaser
|
||||
flags: -v
|
||||
goos:
|
||||
- windows
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
{
|
||||
"recommendations": [
|
||||
"github.copilot"
|
||||
]
|
||||
}
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
# Answer - 构建问答社区
|
||||
|
||||
一款问答形式的知识社区开源软件,用来快速构建产品你的产品技术社区、客户支持社区、用户社区等。
|
||||
一款问答形式的知识社区开源软件,你可以使用它快速建立你的问答社区,用于产品技术支持、客户支持、用户交流等。
|
||||
|
||||
了解更多关于该项目的内容,请访问 [answer.dev](https://answer.dev).
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ import (
|
|||
"github.com/answerdev/answer/internal/install"
|
||||
"github.com/answerdev/answer/internal/migrations"
|
||||
"github.com/answerdev/answer/plugin"
|
||||
"github.com/segmentfault/pacman/log"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
|
@ -22,6 +23,9 @@ var (
|
|||
buildWithPlugins []string
|
||||
// build output path
|
||||
buildOutput string
|
||||
// This config is used to upgrade the database from a specific version manually.
|
||||
// If you want to upgrade the database to version 1.1.0, you can use `answer upgrade -f v1.1.0`.
|
||||
upgradeVersion string
|
||||
)
|
||||
|
||||
func init() {
|
||||
|
@ -35,6 +39,8 @@ func init() {
|
|||
|
||||
buildCmd.Flags().StringVarP(&buildOutput, "output", "o", "", "build output path")
|
||||
|
||||
upgradeCmd.Flags().StringVarP(&upgradeVersion, "from", "f", "", "upgrade from specific version, eg: -f v1.1.0")
|
||||
|
||||
for _, cmd := range []*cobra.Command{initCmd, checkCmd, runCmd, dumpCmd, upgradeCmd, buildCmd, pluginCmd} {
|
||||
rootCmd.AddCommand(cmd)
|
||||
}
|
||||
|
@ -100,13 +106,15 @@ To run answer, use:
|
|||
Short: "upgrade Answer version",
|
||||
Long: `upgrade Answer version`,
|
||||
Run: func(_ *cobra.Command, _ []string) {
|
||||
log.SetLogger(log.NewStdLogger(os.Stdout))
|
||||
cli.FormatAllPath(dataDirPath)
|
||||
cli.InstallI18nBundle(true)
|
||||
c, err := conf.ReadConfig(cli.GetConfigFilePath())
|
||||
if err != nil {
|
||||
fmt.Println("read config failed: ", err.Error())
|
||||
return
|
||||
}
|
||||
if err = migrations.Migrate(c.Data.Database, c.Data.Cache); err != nil {
|
||||
if err = migrations.Migrate(c.Data.Database, c.Data.Cache, upgradeVersion); err != nil {
|
||||
fmt.Println("migrate failed: ", err.Error())
|
||||
return
|
||||
}
|
||||
|
|
|
@ -169,7 +169,7 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database,
|
|||
answerActivityRepo := activity.NewAnswerActivityRepo(dataData, activityRepo, userRankRepo)
|
||||
questionActivityRepo := activity.NewQuestionActivityRepo(dataData, activityRepo, userRankRepo)
|
||||
answerActivityService := activity2.NewAnswerActivityService(answerActivityRepo, questionActivityRepo)
|
||||
questionService := service.NewQuestionService(questionRepo, tagCommonService, questionCommon, userCommon, revisionService, metaService, collectionCommon, answerActivityService, dataData)
|
||||
questionService := service.NewQuestionService(questionRepo, tagCommonService, questionCommon, userCommon, userRepo, revisionService, metaService, collectionCommon, answerActivityService, dataData, emailService)
|
||||
answerService := service.NewAnswerService(answerRepo, questionRepo, questionCommon, userCommon, collectionCommon, userRepo, revisionService, answerActivityService, answerCommon, voteRepo, emailService, userRoleRelService)
|
||||
questionController := controller.NewQuestionController(questionService, answerService, rankService)
|
||||
dashboardService := dashboard.NewDashboardService(questionRepo, answerRepo, commentCommonRepo, voteRepo, userRepo, reportRepo, configRepo, siteInfoCommonService, serviceConf, dataData)
|
||||
|
|
287
docs/docs.go
287
docs/docs.go
|
@ -2841,6 +2841,19 @@ const docTemplate = `{
|
|||
"name": "type",
|
||||
"in": "query",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"enum": [
|
||||
"all",
|
||||
"posts",
|
||||
"invites",
|
||||
"votes"
|
||||
],
|
||||
"type": "string",
|
||||
"description": "inbox_type",
|
||||
"name": "inbox_type",
|
||||
"in": "query",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
|
@ -3588,34 +3601,6 @@ const docTemplate = `{
|
|||
}
|
||||
}
|
||||
},
|
||||
"/answer/api/v1/question/closemsglist": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "close question msg list",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Question"
|
||||
],
|
||||
"summary": "close question msg list",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/handler.RespBody"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/answer/api/v1/question/info": {
|
||||
"get": {
|
||||
"security": [
|
||||
|
@ -3654,6 +3639,81 @@ const docTemplate = `{
|
|||
}
|
||||
}
|
||||
},
|
||||
"/answer/api/v1/question/invite": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "get question invite user info",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Question"
|
||||
],
|
||||
"summary": "get question invite user info",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"default": "1",
|
||||
"description": "Question ID",
|
||||
"name": "id",
|
||||
"in": "query",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"put": {
|
||||
"security": [
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "update question invite user",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Question"
|
||||
],
|
||||
"summary": "update question invite user",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "question",
|
||||
"name": "data",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/schema.QuestionUpdateInviteUser"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/handler.RespBody"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/answer/api/v1/question/operation": {
|
||||
"put": {
|
||||
"security": [
|
||||
|
@ -4049,56 +4109,6 @@ const docTemplate = `{
|
|||
}
|
||||
}
|
||||
},
|
||||
"/answer/api/v1/report/type/list": {
|
||||
"get": {
|
||||
"description": "get report type list",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Report"
|
||||
],
|
||||
"summary": "get report type list",
|
||||
"parameters": [
|
||||
{
|
||||
"enum": [
|
||||
"question",
|
||||
"answer",
|
||||
"comment",
|
||||
"user"
|
||||
],
|
||||
"type": "string",
|
||||
"description": "report source",
|
||||
"name": "source",
|
||||
"in": "query",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/handler.RespBody"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/schema.GetReportTypeResp"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/answer/api/v1/revisions": {
|
||||
"get": {
|
||||
"description": "get revision list",
|
||||
|
@ -5103,6 +5113,55 @@ const docTemplate = `{
|
|||
}
|
||||
}
|
||||
},
|
||||
"/answer/api/v1/user/info/search": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "SearchUserListByName",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"User"
|
||||
],
|
||||
"summary": "SearchUserListByName",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "username",
|
||||
"name": "username",
|
||||
"in": "query",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/handler.RespBody"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"$ref": "#/definitions/schema.GetOtherUserInfoResp"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/answer/api/v1/user/interface": {
|
||||
"put": {
|
||||
"security": [
|
||||
|
@ -6879,35 +6938,6 @@ const docTemplate = `{
|
|||
}
|
||||
}
|
||||
},
|
||||
"schema.GetReportTypeResp": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"content_type": {
|
||||
"description": "content type",
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"description": "report description",
|
||||
"type": "string"
|
||||
},
|
||||
"have_content": {
|
||||
"description": "is have content",
|
||||
"type": "boolean"
|
||||
},
|
||||
"name": {
|
||||
"description": "report name",
|
||||
"type": "string"
|
||||
},
|
||||
"source": {
|
||||
"description": "report source",
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"description": "report type",
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"schema.GetRevisionResp": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
@ -7618,6 +7648,12 @@ const docTemplate = `{
|
|||
"maxLength": 65535,
|
||||
"minLength": 6
|
||||
},
|
||||
"mention_username_list": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"tags": {
|
||||
"description": "tags",
|
||||
"type": "array",
|
||||
|
@ -7781,6 +7817,12 @@ const docTemplate = `{
|
|||
"description": "question id",
|
||||
"type": "string"
|
||||
},
|
||||
"invite_user": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"tags": {
|
||||
"description": "tags",
|
||||
"type": "array",
|
||||
|
@ -7796,6 +7838,23 @@ const docTemplate = `{
|
|||
}
|
||||
}
|
||||
},
|
||||
"schema.QuestionUpdateInviteUser": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id"
|
||||
],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"invite_user": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"schema.RemoveAnswerReq": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
|
@ -8355,6 +8414,9 @@ const docTemplate = `{
|
|||
"system",
|
||||
"gravatar"
|
||||
]
|
||||
},
|
||||
"gravatar_base_url": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -8388,6 +8450,9 @@ const docTemplate = `{
|
|||
"system",
|
||||
"gravatar"
|
||||
]
|
||||
},
|
||||
"gravatar_base_url": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -8925,6 +8990,14 @@ const docTemplate = `{
|
|||
"pass"
|
||||
],
|
||||
"properties": {
|
||||
"captcha_code": {
|
||||
"type": "string",
|
||||
"maxLength": 500
|
||||
},
|
||||
"captcha_id": {
|
||||
"type": "string",
|
||||
"maxLength": 500
|
||||
},
|
||||
"old_pass": {
|
||||
"type": "string",
|
||||
"maxLength": 32,
|
||||
|
|
|
@ -2829,6 +2829,19 @@
|
|||
"name": "type",
|
||||
"in": "query",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"enum": [
|
||||
"all",
|
||||
"posts",
|
||||
"invites",
|
||||
"votes"
|
||||
],
|
||||
"type": "string",
|
||||
"description": "inbox_type",
|
||||
"name": "inbox_type",
|
||||
"in": "query",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
|
@ -3576,34 +3589,6 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"/answer/api/v1/question/closemsglist": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "close question msg list",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Question"
|
||||
],
|
||||
"summary": "close question msg list",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/handler.RespBody"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/answer/api/v1/question/info": {
|
||||
"get": {
|
||||
"security": [
|
||||
|
@ -3642,6 +3627,81 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"/answer/api/v1/question/invite": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "get question invite user info",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Question"
|
||||
],
|
||||
"summary": "get question invite user info",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"default": "1",
|
||||
"description": "Question ID",
|
||||
"name": "id",
|
||||
"in": "query",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"put": {
|
||||
"security": [
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "update question invite user",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Question"
|
||||
],
|
||||
"summary": "update question invite user",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "question",
|
||||
"name": "data",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/schema.QuestionUpdateInviteUser"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/handler.RespBody"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/answer/api/v1/question/operation": {
|
||||
"put": {
|
||||
"security": [
|
||||
|
@ -4037,56 +4097,6 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"/answer/api/v1/report/type/list": {
|
||||
"get": {
|
||||
"description": "get report type list",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Report"
|
||||
],
|
||||
"summary": "get report type list",
|
||||
"parameters": [
|
||||
{
|
||||
"enum": [
|
||||
"question",
|
||||
"answer",
|
||||
"comment",
|
||||
"user"
|
||||
],
|
||||
"type": "string",
|
||||
"description": "report source",
|
||||
"name": "source",
|
||||
"in": "query",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/handler.RespBody"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/schema.GetReportTypeResp"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/answer/api/v1/revisions": {
|
||||
"get": {
|
||||
"description": "get revision list",
|
||||
|
@ -5091,6 +5101,55 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"/answer/api/v1/user/info/search": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "SearchUserListByName",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"User"
|
||||
],
|
||||
"summary": "SearchUserListByName",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "username",
|
||||
"name": "username",
|
||||
"in": "query",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/handler.RespBody"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"$ref": "#/definitions/schema.GetOtherUserInfoResp"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/answer/api/v1/user/interface": {
|
||||
"put": {
|
||||
"security": [
|
||||
|
@ -6867,35 +6926,6 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"schema.GetReportTypeResp": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"content_type": {
|
||||
"description": "content type",
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"description": "report description",
|
||||
"type": "string"
|
||||
},
|
||||
"have_content": {
|
||||
"description": "is have content",
|
||||
"type": "boolean"
|
||||
},
|
||||
"name": {
|
||||
"description": "report name",
|
||||
"type": "string"
|
||||
},
|
||||
"source": {
|
||||
"description": "report source",
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"description": "report type",
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"schema.GetRevisionResp": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
@ -7606,6 +7636,12 @@
|
|||
"maxLength": 65535,
|
||||
"minLength": 6
|
||||
},
|
||||
"mention_username_list": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"tags": {
|
||||
"description": "tags",
|
||||
"type": "array",
|
||||
|
@ -7769,6 +7805,12 @@
|
|||
"description": "question id",
|
||||
"type": "string"
|
||||
},
|
||||
"invite_user": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"tags": {
|
||||
"description": "tags",
|
||||
"type": "array",
|
||||
|
@ -7784,6 +7826,23 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"schema.QuestionUpdateInviteUser": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id"
|
||||
],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"invite_user": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"schema.RemoveAnswerReq": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
|
@ -8343,6 +8402,9 @@
|
|||
"system",
|
||||
"gravatar"
|
||||
]
|
||||
},
|
||||
"gravatar_base_url": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -8376,6 +8438,9 @@
|
|||
"system",
|
||||
"gravatar"
|
||||
]
|
||||
},
|
||||
"gravatar_base_url": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -8913,6 +8978,14 @@
|
|||
"pass"
|
||||
],
|
||||
"properties": {
|
||||
"captcha_code": {
|
||||
"type": "string",
|
||||
"maxLength": 500
|
||||
},
|
||||
"captcha_id": {
|
||||
"type": "string",
|
||||
"maxLength": 500
|
||||
},
|
||||
"old_pass": {
|
||||
"type": "string",
|
||||
"maxLength": 32,
|
||||
|
|
|
@ -693,27 +693,6 @@ definitions:
|
|||
description: url title
|
||||
type: string
|
||||
type: object
|
||||
schema.GetReportTypeResp:
|
||||
properties:
|
||||
content_type:
|
||||
description: content type
|
||||
type: string
|
||||
description:
|
||||
description: report description
|
||||
type: string
|
||||
have_content:
|
||||
description: is have content
|
||||
type: boolean
|
||||
name:
|
||||
description: report name
|
||||
type: string
|
||||
source:
|
||||
description: report source
|
||||
type: string
|
||||
type:
|
||||
description: report type
|
||||
type: integer
|
||||
type: object
|
||||
schema.GetRevisionResp:
|
||||
properties:
|
||||
content:
|
||||
|
@ -1218,6 +1197,10 @@ definitions:
|
|||
maxLength: 65535
|
||||
minLength: 6
|
||||
type: string
|
||||
mention_username_list:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
tags:
|
||||
description: tags
|
||||
items:
|
||||
|
@ -1334,6 +1317,10 @@ definitions:
|
|||
id:
|
||||
description: question id
|
||||
type: string
|
||||
invite_user:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
tags:
|
||||
description: tags
|
||||
items:
|
||||
|
@ -1350,6 +1337,17 @@ definitions:
|
|||
- tags
|
||||
- title
|
||||
type: object
|
||||
schema.QuestionUpdateInviteUser:
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
invite_user:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
required:
|
||||
- id
|
||||
type: object
|
||||
schema.RemoveAnswerReq:
|
||||
properties:
|
||||
id:
|
||||
|
@ -1733,6 +1731,8 @@ definitions:
|
|||
- system
|
||||
- gravatar
|
||||
type: string
|
||||
gravatar_base_url:
|
||||
type: string
|
||||
required:
|
||||
- default_avatar
|
||||
type: object
|
||||
|
@ -1755,6 +1755,8 @@ definitions:
|
|||
- system
|
||||
- gravatar
|
||||
type: string
|
||||
gravatar_base_url:
|
||||
type: string
|
||||
required:
|
||||
- default_avatar
|
||||
type: object
|
||||
|
@ -2134,6 +2136,12 @@ definitions:
|
|||
type: object
|
||||
schema.UserModifyPasswordReq:
|
||||
properties:
|
||||
captcha_code:
|
||||
maxLength: 500
|
||||
type: string
|
||||
captcha_id:
|
||||
maxLength: 500
|
||||
type: string
|
||||
old_pass:
|
||||
maxLength: 32
|
||||
minLength: 8
|
||||
|
@ -3968,6 +3976,16 @@ paths:
|
|||
name: type
|
||||
required: true
|
||||
type: string
|
||||
- description: inbox_type
|
||||
enum:
|
||||
- all
|
||||
- posts
|
||||
- invites
|
||||
- votes
|
||||
in: query
|
||||
name: inbox_type
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
|
@ -4426,23 +4444,6 @@ paths:
|
|||
summary: add question and answer
|
||||
tags:
|
||||
- Question
|
||||
/answer/api/v1/question/closemsglist:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: close question msg list
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/handler.RespBody'
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
summary: close question msg list
|
||||
tags:
|
||||
- Question
|
||||
/answer/api/v1/question/info:
|
||||
get:
|
||||
consumes:
|
||||
|
@ -4467,6 +4468,53 @@ paths:
|
|||
summary: get question details
|
||||
tags:
|
||||
- Question
|
||||
/answer/api/v1/question/invite:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: get question invite user info
|
||||
parameters:
|
||||
- default: "1"
|
||||
description: Question ID
|
||||
in: query
|
||||
name: id
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
type: string
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
summary: get question invite user info
|
||||
tags:
|
||||
- Question
|
||||
put:
|
||||
consumes:
|
||||
- application/json
|
||||
description: update question invite user
|
||||
parameters:
|
||||
- description: question
|
||||
in: body
|
||||
name: data
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/schema.QuestionUpdateInviteUser'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/handler.RespBody'
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
summary: update question invite user
|
||||
tags:
|
||||
- Question
|
||||
/answer/api/v1/question/operation:
|
||||
put:
|
||||
consumes:
|
||||
|
@ -4709,37 +4757,6 @@ paths:
|
|||
summary: add report
|
||||
tags:
|
||||
- Report
|
||||
/answer/api/v1/report/type/list:
|
||||
get:
|
||||
description: get report type list
|
||||
parameters:
|
||||
- description: report source
|
||||
enum:
|
||||
- question
|
||||
- answer
|
||||
- comment
|
||||
- user
|
||||
in: query
|
||||
name: source
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: '#/definitions/handler.RespBody'
|
||||
- properties:
|
||||
data:
|
||||
items:
|
||||
$ref: '#/definitions/schema.GetReportTypeResp'
|
||||
type: array
|
||||
type: object
|
||||
summary: get report type list
|
||||
tags:
|
||||
- Report
|
||||
/answer/api/v1/revisions:
|
||||
get:
|
||||
description: get revision list
|
||||
|
@ -5350,6 +5367,34 @@ paths:
|
|||
summary: UserUpdateInfo update user info
|
||||
tags:
|
||||
- User
|
||||
/answer/api/v1/user/info/search:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: SearchUserListByName
|
||||
parameters:
|
||||
- description: username
|
||||
in: query
|
||||
name: username
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: '#/definitions/handler.RespBody'
|
||||
- properties:
|
||||
data:
|
||||
$ref: '#/definitions/schema.GetOtherUserInfoResp'
|
||||
type: object
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
summary: SearchUserListByName
|
||||
tags:
|
||||
- User
|
||||
/answer/api/v1/user/interface:
|
||||
put:
|
||||
consumes:
|
||||
|
|
10
go.mod
10
go.mod
|
@ -29,10 +29,10 @@ require (
|
|||
github.com/ory/dockertest/v3 v3.9.1
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/scottleedavis/go-exif-remove v0.0.0-20230314195146-7e059d593405
|
||||
github.com/segmentfault/pacman v1.0.3
|
||||
github.com/segmentfault/pacman v1.0.4
|
||||
github.com/segmentfault/pacman/contrib/cache/memory v0.0.0-20221219081300-f734f4a16aa0
|
||||
github.com/segmentfault/pacman/contrib/conf/viper v0.0.0-20221018072427-a15dd1434e05
|
||||
github.com/segmentfault/pacman/contrib/i18n v0.0.0-20221219081300-f734f4a16aa0
|
||||
github.com/segmentfault/pacman/contrib/i18n v0.0.0-20230516093754-b76aef1c1150
|
||||
github.com/segmentfault/pacman/contrib/log/zap v0.0.0-20221018072427-a15dd1434e05
|
||||
github.com/segmentfault/pacman/contrib/server/http v0.0.0-20221018072427-a15dd1434e05
|
||||
github.com/spf13/cobra v1.6.1
|
||||
|
@ -129,10 +129,10 @@ require (
|
|||
go.uber.org/multierr v1.8.0 // indirect
|
||||
go.uber.org/zap v1.23.0 // indirect
|
||||
golang.org/x/image v0.1.0 // indirect
|
||||
golang.org/x/mod v0.6.0 // indirect
|
||||
golang.org/x/mod v0.8.0 // indirect
|
||||
golang.org/x/sys v0.5.0 // indirect
|
||||
golang.org/x/text v0.7.0 // indirect
|
||||
golang.org/x/tools v0.2.0 // indirect
|
||||
golang.org/x/text v0.9.0 // indirect
|
||||
golang.org/x/tools v0.6.0 // indirect
|
||||
google.golang.org/protobuf v1.28.1 // indirect
|
||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
|
|
20
go.sum
20
go.sum
|
@ -627,12 +627,16 @@ github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg
|
|||
github.com/seccomp/libseccomp-golang v0.9.2-0.20210429002308-3879420cc921/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg=
|
||||
github.com/segmentfault/pacman v1.0.3 h1:/K8LJHQMiCaCIvC/e8GQITpYTEG6RH4KTLTZjPTghl4=
|
||||
github.com/segmentfault/pacman v1.0.3/go.mod h1:5lNp5REd8QMThmBUvR3Fi9Y3AsOB4GRq7soCB4QLqOs=
|
||||
github.com/segmentfault/pacman v1.0.4 h1:6UIXuMHUeYMWe5toflV9SXZQizRny1RczjZJLj9kul0=
|
||||
github.com/segmentfault/pacman v1.0.4/go.mod h1:5lNp5REd8QMThmBUvR3Fi9Y3AsOB4GRq7soCB4QLqOs=
|
||||
github.com/segmentfault/pacman/contrib/cache/memory v0.0.0-20221219081300-f734f4a16aa0 h1:4x0qG7H2M3qH7Yo2BhGrVlji1iTmRAWgINY/JyENeHs=
|
||||
github.com/segmentfault/pacman/contrib/cache/memory v0.0.0-20221219081300-f734f4a16aa0/go.mod h1:rmf1TCwz67dyM+AmTwSd1BxTo2AOYHj262lP93bOZbs=
|
||||
github.com/segmentfault/pacman/contrib/conf/viper v0.0.0-20221018072427-a15dd1434e05 h1:BlqTgc3/MYKG6vMI2MI+6o+7P4Gy5PXlawu185wPXAk=
|
||||
github.com/segmentfault/pacman/contrib/conf/viper v0.0.0-20221018072427-a15dd1434e05/go.mod h1:prPjFam7MyZ5b3S9dcDOt2tMPz6kf7C9c243s9zSwPY=
|
||||
github.com/segmentfault/pacman/contrib/i18n v0.0.0-20221219081300-f734f4a16aa0 h1:zaAwBSpwUVrV2BBs1f1hfkv0rY/KdZLyKK8U9NKiurI=
|
||||
github.com/segmentfault/pacman/contrib/i18n v0.0.0-20221219081300-f734f4a16aa0/go.mod h1:7QcRmnV7OYq4hNOOCWXT5HXnN/u756JUsqIW0Bw8n9E=
|
||||
github.com/segmentfault/pacman/contrib/i18n v0.0.0-20230516093245-f9384b820548 h1:R+FH23Qrdp5ECuHXmZy4BvoO/x7m2wZgNeiC46+jqCQ=
|
||||
github.com/segmentfault/pacman/contrib/i18n v0.0.0-20230516093245-f9384b820548/go.mod h1:7QcRmnV7OYq4hNOOCWXT5HXnN/u756JUsqIW0Bw8n9E=
|
||||
github.com/segmentfault/pacman/contrib/i18n v0.0.0-20230516093754-b76aef1c1150 h1:OEuW1D7RGDE0CZDr0oGMw9Eiq7fAbD9C4WMrvSixamk=
|
||||
github.com/segmentfault/pacman/contrib/i18n v0.0.0-20230516093754-b76aef1c1150/go.mod h1:7QcRmnV7OYq4hNOOCWXT5HXnN/u756JUsqIW0Bw8n9E=
|
||||
github.com/segmentfault/pacman/contrib/log/zap v0.0.0-20221018072427-a15dd1434e05 h1:jcGZU2juv0L3eFEkuZYV14ESLUlWfGMWnP0mjOfrSZc=
|
||||
github.com/segmentfault/pacman/contrib/log/zap v0.0.0-20221018072427-a15dd1434e05/go.mod h1:L4GqtXLoR73obTYqUQIzfkm8NG8pvZafxFb6KZFSSHk=
|
||||
github.com/segmentfault/pacman/contrib/server/http v0.0.0-20221018072427-a15dd1434e05 h1:91is1nKNbfTOl8CvMYiFgg4c5Vmol+5mVmMV/jDXD+A=
|
||||
|
@ -822,8 +826,8 @@ golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
|||
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.6.0 h1:b9gGHsz9/HhJ3HF5DHQytPpuwocVTChQJK3AvoLRD5I=
|
||||
golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI=
|
||||
golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
|
@ -893,6 +897,7 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ
|
|||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
|
||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
|
@ -981,8 +986,9 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
|||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
|
@ -1050,8 +1056,8 @@ golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
|
|||
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.2.0 h1:G6AHpWxTMGY1KyEYoAQ5WTtIekUUvDNjan3ugu60JvE=
|
||||
golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA=
|
||||
golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
# The following fields are used for back-end
|
||||
|
||||
backend:
|
||||
base:
|
||||
success:
|
||||
|
@ -35,6 +34,8 @@ backend:
|
|||
other: Unpin
|
||||
show:
|
||||
other: List
|
||||
invite_someone_to_answer:
|
||||
other: Edit
|
||||
role:
|
||||
name:
|
||||
user:
|
||||
|
@ -80,6 +81,8 @@ backend:
|
|||
other: Downvote question
|
||||
rank_answer_vote_down_label:
|
||||
other: Downvote answer
|
||||
rank_invite_someone_to_answer_label:
|
||||
other: Invite someone to answer
|
||||
rank_tag_add_label:
|
||||
other: Create new tag
|
||||
rank_tag_edit_label:
|
||||
|
@ -109,6 +112,9 @@ backend:
|
|||
email_or_password_wrong_error:
|
||||
other: Email and password do not match.
|
||||
error:
|
||||
password:
|
||||
space_invalid:
|
||||
other: Password cannot contain spaces.
|
||||
admin:
|
||||
cannot_update_their_password:
|
||||
other: You cannot modify your password.
|
||||
|
@ -250,41 +256,74 @@ backend:
|
|||
upload:
|
||||
unsupported_file_format:
|
||||
other: Unsupported file format.
|
||||
report:
|
||||
reason:
|
||||
spam:
|
||||
name:
|
||||
other: spam
|
||||
desc:
|
||||
other: This post is an advertisement, or vandalism. It is not useful or relevant
|
||||
to the current topic.
|
||||
rude:
|
||||
other: This post is an advertisement, or vandalism. It is not useful or relevant to the current topic.
|
||||
rude_or_abusive:
|
||||
name:
|
||||
other: rude or abusive
|
||||
desc:
|
||||
other: A reasonable person would find this content inappropriate for respectful
|
||||
discourse.
|
||||
duplicate:
|
||||
a_duplicate:
|
||||
name:
|
||||
other: a duplicate
|
||||
desc:
|
||||
other: This question has been asked before and already has an answer.
|
||||
not_answer:
|
||||
placeholder:
|
||||
other: Enter the existing question link
|
||||
not_a_answer:
|
||||
name:
|
||||
other: not an answer
|
||||
desc:
|
||||
other: This was posted as an answer, but it does not attempt to answer the
|
||||
question. It should possibly be an edit, a comment, another question,
|
||||
or deleted altogether.
|
||||
not_need:
|
||||
no_longer_needed:
|
||||
name:
|
||||
other: no longer needed
|
||||
desc:
|
||||
other: This comment is outdated, conversational or not relevant to this post.
|
||||
other:
|
||||
something:
|
||||
name:
|
||||
other: something else
|
||||
desc:
|
||||
other: This post requires staff attention for another reason not listed above.
|
||||
placeholder:
|
||||
other: Let us know specifically what you are concerned about
|
||||
community_specific:
|
||||
name:
|
||||
other: a community-specific reason
|
||||
desc:
|
||||
other: This question doesn’t meet a community guideline.
|
||||
not_clarity:
|
||||
name:
|
||||
other: needs details or clarity
|
||||
desc:
|
||||
other: This question currently includes multiple questions in one. It should focus on one problem only.
|
||||
looks_ok:
|
||||
name:
|
||||
other: looks ok
|
||||
desc:
|
||||
other: This post is good as-is and not low quality.
|
||||
needs_edit:
|
||||
name:
|
||||
other: needs edit, and I did it
|
||||
desc:
|
||||
other: Improve and correct problems with this post yourself.
|
||||
needs_close:
|
||||
name:
|
||||
other: needs close
|
||||
desc:
|
||||
other: A closed question can’t answer, but still can edit, vote and comment.
|
||||
needs_delete:
|
||||
name:
|
||||
other: needs delete
|
||||
desc:
|
||||
other: All reputation gained and lost will be restored.
|
||||
question:
|
||||
close:
|
||||
duplicate:
|
||||
|
@ -351,6 +390,44 @@ backend:
|
|||
other: downvoted answer
|
||||
up_voted_comment:
|
||||
other: upvoted comment
|
||||
invited_you_to_answer:
|
||||
other: invited you to answer
|
||||
email_tpl:
|
||||
change_email:
|
||||
title:
|
||||
other: "[{{.SiteName}}] Confirm your new email address"
|
||||
body:
|
||||
other: "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"
|
||||
new_answer:
|
||||
title:
|
||||
other: "[{{.SiteName}}] {{.DisplayName}} answered your question"
|
||||
body:
|
||||
other: "<strong><a href='{{.AnswerUrl}}'>{{.QuestionTitle}}</a></strong><br><br>\n\n<small>{{.DisplayName}}:</small><br>\n<blockquote>{{.AnswerSummary}}</blockquote><br>\n<a href='{{.AnswerUrl}}'>View it on {{.SiteName}}</a><br><br>\n\n<small>You are receiving this because you authored the thread. <a href='{{.UnsubscribeUrl}}'>Unsubscribe</a></small>"
|
||||
invited_you_to_answer:
|
||||
title:
|
||||
other: "[{{.SiteName}}] {{.DisplayName}} invited you to answer"
|
||||
body:
|
||||
other: "<strong><a href='{{.InviteUrl}}'>{{.QuestionTitle}}</a></strong><br><br>\n\n<small>{{.DisplayName}}:</small><br>\n<blockquote>I think you may know the answer.</blockquote><br>\n<a href='{{.InviteUrl}}'>View it on {{.SiteName}}</a><br><br>\n\n<small>You are receiving this because you authored the thread. <a href='{{.UnsubscribeUrl}}'>Unsubscribe</a></small>"
|
||||
new_comment:
|
||||
title:
|
||||
other: "[{{.SiteName}}] {{.DisplayName}} commented on your post"
|
||||
body:
|
||||
other: "<strong><a href='{{.CommentUrl}}'>{{.QuestionTitle}}</a></strong><br><br>\n\n<small>{{.DisplayName}}:</small><br>\n<blockquote>{{.CommentSummary}}</blockquote><br>\n<a href='{{.CommentUrl}}'>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>"
|
||||
pass_reset:
|
||||
title:
|
||||
other: "[{{.SiteName }}] Password reset"
|
||||
body:
|
||||
other: "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"
|
||||
register:
|
||||
title:
|
||||
other: "[{{.SiteName}}] Confirm your new account"
|
||||
body:
|
||||
other: "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"
|
||||
test:
|
||||
title:
|
||||
other: "[{{.SiteName}}] Test Email"
|
||||
body:
|
||||
other: "This is a test email."
|
||||
|
||||
# The following fields are used for interface presentation(Front-end)
|
||||
ui:
|
||||
|
|
|
@ -75,6 +75,8 @@ backend:
|
|||
other: 问题点踩
|
||||
rank_answer_vote_down_label:
|
||||
other: 答案点踩
|
||||
rank_invite_someone_to_answer_label:
|
||||
other: 邀请回答
|
||||
rank_tag_add_label:
|
||||
other: 创建新标签
|
||||
rank_tag_edit_label:
|
||||
|
@ -97,13 +99,16 @@ backend:
|
|||
other: 编辑标签描述(无需审核)
|
||||
rank_tag_synonym_label:
|
||||
other: 管理标签同义词
|
||||
email:
|
||||
e_mail:
|
||||
other: 邮箱
|
||||
password:
|
||||
other: 密码
|
||||
email_or_password_wrong_error:
|
||||
other: 邮箱和密码不匹配。
|
||||
error:
|
||||
password:
|
||||
space_invalid:
|
||||
other: 密码不能包含空格。
|
||||
admin:
|
||||
cannot_update_their_password:
|
||||
other: 您无法修改自己的密码。
|
||||
|
@ -243,37 +248,71 @@ backend:
|
|||
upload:
|
||||
unsupported_file_format:
|
||||
other: 不支持的文件格式。
|
||||
report:
|
||||
reason:
|
||||
spam:
|
||||
name:
|
||||
other: 垃圾信息
|
||||
desc:
|
||||
other: 这个帖子是一个广告,或是破坏性行为。它对当前的主题没有用处,也不相关。
|
||||
rude:
|
||||
rude_or_abusive:
|
||||
name:
|
||||
other: 粗鲁或辱骂的
|
||||
desc:
|
||||
other: 一个有理智的人都会认为这种内容不适合进行尊重性的讨论。
|
||||
duplicate:
|
||||
a_duplicate:
|
||||
name:
|
||||
other: 重复信息
|
||||
desc:
|
||||
other: 此问题以前就有人问过,而且已经有了答案。
|
||||
not_answer:
|
||||
placeholder:
|
||||
other: 请输入重复的问题的网址
|
||||
not_a_answer:
|
||||
name:
|
||||
other: 不是答案
|
||||
desc:
|
||||
other: 此帖子是作为一个答案发布的,但它并没有试图回答这个问题。总之,它可能应该是个编辑,评论,另一个问题或者被删除。
|
||||
not_need:
|
||||
no_longer_needed:
|
||||
name:
|
||||
other: 不再需要
|
||||
desc:
|
||||
other: 此评论已过时,对话或与此帖子无关。
|
||||
other:
|
||||
something:
|
||||
name:
|
||||
other: 其他原因
|
||||
desc:
|
||||
other: 此帖子需要工作人员关注,因为是上述所列以外的其他理由。
|
||||
placeholder:
|
||||
other: 让我们具体了解你所关注的内容
|
||||
community_specific:
|
||||
name:
|
||||
other: 特定社区原因
|
||||
desc:
|
||||
other: 这个问题不符合社区准则。
|
||||
not_clarity:
|
||||
name:
|
||||
other: 需要细节或澄清
|
||||
desc:
|
||||
other: 此问题目前包含多个问题。它应该只关注一个问题。
|
||||
looks_ok:
|
||||
name:
|
||||
other: 看起来不错
|
||||
desc:
|
||||
other: 这篇文章很好,不是低质量的。
|
||||
needs_edit:
|
||||
name:
|
||||
other: 需要编辑,我已经编辑了
|
||||
desc:
|
||||
other: 自己改善和纠正这篇文章中的问题。
|
||||
needs_close:
|
||||
name:
|
||||
other: 需要关闭
|
||||
desc:
|
||||
other: 关闭的问题不能回答,但仍然可以编辑、投票和评论。
|
||||
needs_delete:
|
||||
name:
|
||||
other: 需要删除
|
||||
desc:
|
||||
other: 所有获得和失去的声望都将被恢复。
|
||||
question:
|
||||
close:
|
||||
duplicate:
|
||||
|
@ -339,6 +378,44 @@ backend:
|
|||
other: 踩了答案
|
||||
up_voted_comment:
|
||||
other: 赞了评论
|
||||
invited_you_to_answer:
|
||||
other: 邀请你回答问题
|
||||
email_tpl:
|
||||
change_email:
|
||||
title:
|
||||
other: "[{{.SiteName}}] 确认您的新电子邮件地址"
|
||||
body:
|
||||
other: "请点击以下链接确认您在 {{.SiteName}} 上的新电子邮件地址:<br><br>\n\n<a href='{{.ChangeEmailUrl}}' target='_blank'>{{.ChangeEmailUrl}}</a><br><br>\n\n如果您没有请求此更改,请忽略此电子邮件。\n"
|
||||
new_answer:
|
||||
title:
|
||||
other: "[{{.SiteName}}] {{.DisplayName}} 回答了您的问题"
|
||||
body:
|
||||
other: "<strong><a href='{{.AnswerUrl}}'>{{.QuestionTitle}}</a></strong><br><br>\n\n<small>{{.DisplayName}}:</small><br>\n<blockquote>{{.AnswerSummary}}</blockquote><br>\n<a href='{{.AnswerUrl}}'>在 {{.SiteName}} 上查看</a><br><br>\n\n<small>您会收到此邮件是因为您开启了订阅。<a href='{{.UnsubscribeUrl}}'>取消订阅</a></small>"
|
||||
invited_you_to_answer:
|
||||
title:
|
||||
other: "[{{.SiteName}}] {{.DisplayName}} 邀请您回答问题"
|
||||
body:
|
||||
other: "<strong><a href='{{.InviteUrl}}'>{{.QuestionTitle}}</a></strong><br><br>\n\n<small>{{.DisplayName}}:</small><br>\n<blockquote>我想你可能知道答案。</blockquote><br>\n<a href='{{.InviteUrl}}'>在 {{.SiteName}} 上查看</a><br><br>\n\n<small>您会收到此邮件是因为您开启了订阅. <a href='{{.UnsubscribeUrl}}'>取消订阅</a></small>"
|
||||
new_comment:
|
||||
title:
|
||||
other: "[{{.SiteName}}] {{.DisplayName}} 评论了您的帖子"
|
||||
body:
|
||||
other: "<strong><a href='{{.CommentUrl}}'>{{.QuestionTitle}}</a></strong><br><br>\n\n<small>{{.DisplayName}}:</small><br>\n<blockquote>{{.CommentSummary}}</blockquote><br>\n<a href='{{.CommentUrl}}'>在 {{.SiteName}} 上查看</a><br><br>\n\n<small>您会收到此邮件是因为您开启了订阅。<a href='{{.UnsubscribeUrl}}'>取消订阅</a></small>"
|
||||
pass_reset:
|
||||
title:
|
||||
other: "[{{.SiteName }}] 重置密码"
|
||||
body:
|
||||
other: "有人要求在 [{{.SiteName}}] 上重置您的密码。<br><br>\n\n如果这不是您的操作,请安心忽略此电子邮件。<br><br>\n\n请点击以下链接选择一个新密码:<br>\n<a href='{{.PassResetUrl}}' target='_blank'>{{.PassResetUrl}}</a>\n"
|
||||
register:
|
||||
title:
|
||||
other: "[{{.SiteName}}] 确认您的新账户"
|
||||
body:
|
||||
other: "欢迎加入 {{.SiteName}}<br><br>\n\n请点击以下链接确认并激活您的新账户:<br>\n<a href='{{.RegisterUrl}}' target='_blank'>{{.RegisterUrl}}</a><br><br>\n\n如果上面的链接不能点击,请将其复制并粘贴到您的浏览器地址栏中。\n"
|
||||
test:
|
||||
title:
|
||||
other: "[{{.SiteName}}] 测试邮件"
|
||||
body:
|
||||
other: "这是一封测试邮件。"
|
||||
#The following fields are used for interface presentation(Front-end)
|
||||
ui:
|
||||
how_to_format:
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
package constant
|
||||
|
||||
const (
|
||||
EmailTplKeyChangeEmailTitle = "email_tpl.change_email.title"
|
||||
EmailTplKeyChangeEmailBody = "email_tpl.change_email.body"
|
||||
|
||||
EmailTplKeyNewAnswerTitle = "email_tpl.new_answer.title"
|
||||
EmailTplKeyNewAnswerBody = "email_tpl.new_answer.body"
|
||||
|
||||
EmailTplKeyNewCommentTitle = "email_tpl.new_comment.title"
|
||||
EmailTplKeyNewCommentBody = "email_tpl.new_comment.body"
|
||||
|
||||
EmailTplKeyPassResetTitle = "email_tpl.pass_reset.title"
|
||||
EmailTplKeyPassResetBody = "email_tpl.pass_reset.body"
|
||||
|
||||
EmailTplKeyRegisterTitle = "email_tpl.register.title"
|
||||
EmailTplKeyRegisterBody = "email_tpl.register.body"
|
||||
|
||||
EmailTplKeyTestTitle = "email_tpl.test.title"
|
||||
EmailTplKeyTestBody = "email_tpl.test.body"
|
||||
|
||||
EmailTplKeyInvitedAnswerTitle = "email_tpl.invited_you_to_answer.title"
|
||||
EmailTplKeyInvitedAnswerBody = "email_tpl.invited_you_to_answer.body"
|
||||
)
|
|
@ -35,4 +35,6 @@ const (
|
|||
NotificationYourAnswerWasDeleted = "notification.action.your_answer_was_deleted"
|
||||
// NotificationYourCommentWasDeleted your comment was deleted
|
||||
NotificationYourCommentWasDeleted = "notification.action.your_comment_was_deleted"
|
||||
// NotificationInvitedYouToAnswer invited you to answer
|
||||
NotificationInvitedYouToAnswer = "notification.action.invited_you_to_answer"
|
||||
)
|
||||
|
|
|
@ -20,6 +20,7 @@ const (
|
|||
RankAnswerAcceptKey = "rank.answer.accept"
|
||||
RankAnswerVoteUpKey = "rank.answer.vote_up"
|
||||
RankAnswerVoteDownKey = "rank.answer.vote_down"
|
||||
RankInviteSomeoneToAnswerKey = "rank.answer.invite_someone_to_answer"
|
||||
RankCommentAddKey = "rank.comment.add"
|
||||
RankCommentEditKey = "rank.comment.edit"
|
||||
RankCommentDeleteKey = "rank.comment.delete"
|
||||
|
@ -55,6 +56,7 @@ var (
|
|||
{Label: reason.RankAnswerVoteUpLabel, Key: RankAnswerVoteUpKey},
|
||||
{Label: reason.RankQuestionVoteDownLabel, Key: RankQuestionVoteDownKey},
|
||||
{Label: reason.RankAnswerVoteDownLabel, Key: RankAnswerVoteDownKey},
|
||||
{Label: reason.RankInviteSomeoneToAnswerLabel, Key: RankInviteSomeoneToAnswerKey},
|
||||
{Label: reason.RankTagAddLabel, Key: RankTagAddKey},
|
||||
{Label: reason.RankTagEditLabel, Key: RankTagEditKey},
|
||||
{Label: reason.RankQuestionEditLabel, Key: RankQuestionEditKey},
|
||||
|
|
|
@ -1,34 +0,0 @@
|
|||
package constant
|
||||
|
||||
const (
|
||||
ReportSpamName = "report.spam.name"
|
||||
ReportSpamDescription = "report.spam.description"
|
||||
ReportRudeName = "report.rude.name"
|
||||
ReportRudeDescription = "report.rude.description"
|
||||
ReportDuplicateName = "report.duplicate.name"
|
||||
ReportDuplicateDescription = "report.duplicate.description"
|
||||
ReportOtherName = "report.other.name"
|
||||
ReportOtherDescription = "report.other.description"
|
||||
ReportNotAnswerName = "report.not_answer.name"
|
||||
ReportNotAnswerDescription = "report.not_answer.description"
|
||||
ReportNotNeedName = "report.not_need.name"
|
||||
ReportNotNeedDescription = "report.not_need.description"
|
||||
// question close
|
||||
QuestionCloseDuplicateName = "question.close.duplicate.name"
|
||||
QuestionCloseDuplicateDescription = "question.close.duplicate.description"
|
||||
QuestionCloseGuidelineName = "question.close.guideline.name"
|
||||
QuestionCloseGuidelineDescription = "question.close.guideline.description"
|
||||
QuestionCloseMultipleName = "question.close.multiple.name"
|
||||
QuestionCloseMultipleDescription = "question.close.multiple.description"
|
||||
QuestionCloseOtherName = "question.close.other.name"
|
||||
QuestionCloseOtherDescription = "question.close.other.description"
|
||||
)
|
||||
|
||||
const (
|
||||
// TODO put this in database
|
||||
// TODO need reason controller to resolve
|
||||
QuestionCloseJSON = `[{"name":"question.close.duplicate.name","description":"question.close.duplicate.description","source":"question","type":1,"have_content":false,"content_type":""},{"name":"question.close.guideline.name","description":"question.close.guideline.description","source":"question","type":2,"have_content":false,"content_type":""},{"name":"question.close.multiple.name","description":"question.close.multiple.description","source":"question","type":3,"have_content":true,"content_type":"text"},{"name":"question.close.other.name","description":"question.close.other.description","source":"question","type":4,"have_content":true,"content_type":"textarea"}]`
|
||||
QuestionReportJSON = `[{"name":"report.spam.name","description":"report.spam.description","source":"question","type":1,"have_content":false,"content_type":""},{"name":"report.rude.name","description":"report.rude.description","source":"question","type":2,"have_content":false,"content_type":""},{"name":"report.duplicate.name","description":"report.duplicate.description","source":"question","type":3,"have_content":true,"content_type":"text"},{"name":"report.other.name","description":"report.other.description","source":"question","type":4,"have_content":true,"content_type":"textarea"}]`
|
||||
AnswerReportJSON = `[{"name":"report.spam.name","description":"report.spam.description","source":"answer","type":1,"have_content":false,"content_type":""},{"name":"report.rude.name","description":"report.rude.description","source":"answer","type":2,"have_content":false,"content_type":""},{"name":"report.not_answer.name","description":"report.not_answer.description","source":"answer","type":3,"have_content":false,"content_type":""},{"name":"report.other.name","description":"report.other.description","source":"answer","type":4,"have_content":true,"content_type":"textarea"}]`
|
||||
CommentReportJSON = `[{"name":"report.spam.name","description":"report.spam.description","source":"comment","type":1,"have_content":false,"content_type":""},{"name":"report.rude.name","description":"report.rude.description","source":"comment","type":2,"have_content":false,"content_type":""},{"name":"report.not_need.name","description":"report.not_need.description","source":"comment","type":3,"have_content":true,"content_type":"text"},{"name":"report.other.name","description":"report.other.description","source":"comment","type":4,"have_content":true,"content_type":"textarea"}]`
|
||||
)
|
|
@ -1,6 +1,7 @@
|
|||
package constant
|
||||
|
||||
var (
|
||||
DefaultAvatar = "system"
|
||||
DefaultSiteURL = ""
|
||||
DefaultAvatar = "system"
|
||||
DefaultGravatarBaseURL = "https://www.gravatar.com/avatar/"
|
||||
DefaultSiteURL = ""
|
||||
)
|
||||
|
|
|
@ -15,6 +15,7 @@ const (
|
|||
RankAnswerVoteUpLabel = "privilege.rank_answer_vote_up_label"
|
||||
RankQuestionVoteDownLabel = "privilege.rank_question_vote_down_label"
|
||||
RankAnswerVoteDownLabel = "privilege.rank_answer_vote_down_label"
|
||||
RankInviteSomeoneToAnswerLabel = "privilege.rank_invite_someone_to_answer_label"
|
||||
RankTagAddLabel = "privilege.rank_tag_add_label"
|
||||
RankTagEditLabel = "privilege.rank_tag_edit_label"
|
||||
RankQuestionEditLabel = "privilege.rank_question_edit_label"
|
||||
|
|
|
@ -131,3 +131,15 @@ func Tr(lang i18n.Language, data string) string {
|
|||
}
|
||||
return translation
|
||||
}
|
||||
|
||||
// TrWithData translate key with template data, it will replace the template data {{ .PlaceHolder }} in the translation.
|
||||
func TrWithData(lang i18n.Language, key string, templateData any) string {
|
||||
if GlobalTrans == nil {
|
||||
return key
|
||||
}
|
||||
translation := GlobalTrans.TrWithData(lang, key, templateData)
|
||||
if translation == key {
|
||||
return GlobalTrans.TrWithData(i18n.DefaultLanguage, key, templateData)
|
||||
}
|
||||
return translation
|
||||
}
|
||||
|
|
|
@ -207,10 +207,12 @@ func (m *MyValidator) Check(value interface{}) (errFields []*FormErrorField, err
|
|||
if err == nil {
|
||||
return nil, nil
|
||||
}
|
||||
errMsg := ""
|
||||
for _, errField := range errFields {
|
||||
errField.ErrorMsg = translator.Tr(m.Lang, errField.ErrorMsg)
|
||||
errMsg = errField.ErrorMsg
|
||||
}
|
||||
return errFields, err
|
||||
return errFields, myErrors.BadRequest(reason.RequestFormatError).WithMsg(errMsg)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
|
|
@ -40,7 +40,7 @@ func FormatAllPath(dataDirPath string) {
|
|||
func InstallAllInitialEnvironment(dataDirPath string) {
|
||||
FormatAllPath(dataDirPath)
|
||||
installUploadDir()
|
||||
installI18nBundle()
|
||||
InstallI18nBundle(false)
|
||||
fmt.Println("install all initial environment done")
|
||||
}
|
||||
|
||||
|
@ -79,7 +79,7 @@ func installUploadDir() {
|
|||
}
|
||||
}
|
||||
|
||||
func installI18nBundle() {
|
||||
func InstallI18nBundle(replace bool) {
|
||||
fmt.Println("[i18n] try to install i18n bundle...")
|
||||
if err := dir.CreateDirIfNotExist(I18nPath); err != nil {
|
||||
fmt.Println(err.Error())
|
||||
|
@ -98,7 +98,11 @@ func installI18nBundle() {
|
|||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if dir.CheckFileExist(path) {
|
||||
exist := dir.CheckFileExist(path)
|
||||
if exist && !replace {
|
||||
continue
|
||||
}
|
||||
if exist {
|
||||
fmt.Printf("[i18n] install %s file exist, try to replace it\n", item.Name())
|
||||
if err = os.Remove(path); err != nil {
|
||||
fmt.Println(err)
|
||||
|
|
|
@ -109,7 +109,7 @@ func (cc *ConnectorController) ConnectorRedirect(connector plugin.Connector) (fn
|
|||
commonRouterPrefix, ConnectorRedirectRouterPrefix, connector.ConnectorSlugName())
|
||||
userInfo, err := connector.ConnectorReceiver(ctx, receiverURL)
|
||||
if err != nil {
|
||||
log.Errorf("connector received failed: %v", err)
|
||||
log.Errorf("connector received failed, error info: %v, response data is: %s", err, userInfo.MetaInfo)
|
||||
ctx.Redirect(http.StatusFound, "/50x")
|
||||
return
|
||||
}
|
||||
|
|
|
@ -143,6 +143,7 @@ func (nc *NotificationController) ClearIDUnRead(ctx *gin.Context) {
|
|||
// @Param page query int false "page size"
|
||||
// @Param page_size query int false "page size"
|
||||
// @Param type query string true "type" Enums(inbox,achievement)
|
||||
// @Param inbox_type query string true "inbox_type" Enums(all,posts,invites,votes)
|
||||
// @Success 200 {object} handler.RespBody
|
||||
// @Router /answer/api/v1/notification/page [get]
|
||||
func (nc *NotificationController) GetList(ctx *gin.Context) {
|
||||
|
|
|
@ -196,6 +196,7 @@ func (qc *QuestionController) GetQuestion(ctx *gin.Context) {
|
|||
permission.QuestionUnPin,
|
||||
permission.QuestionHide,
|
||||
permission.QuestionShow,
|
||||
permission.AnswerInviteSomeoneToAnswer,
|
||||
})
|
||||
if err != nil {
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
|
@ -211,6 +212,7 @@ func (qc *QuestionController) GetQuestion(ctx *gin.Context) {
|
|||
req.CanUnPin = canList[5]
|
||||
req.CanHide = canList[6]
|
||||
req.CanShow = canList[7]
|
||||
req.CanInviteOtherToAnswer = canList[8]
|
||||
|
||||
info, err := qc.questionService.GetQuestionAndAddPV(ctx, id, userID, req)
|
||||
if err != nil {
|
||||
|
@ -221,6 +223,23 @@ func (qc *QuestionController) GetQuestion(ctx *gin.Context) {
|
|||
handler.HandleResponse(ctx, nil, info)
|
||||
}
|
||||
|
||||
// GetQuestionInviteUserInfo get question invite user info
|
||||
// @Summary get question invite user info
|
||||
// @Description get question invite user info
|
||||
// @Tags Question
|
||||
// @Security ApiKeyAuth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id query string true "Question ID" default(1)
|
||||
// @Success 200 {string} string ""
|
||||
// @Router /answer/api/v1/question/invite [get]
|
||||
func (qc *QuestionController) GetQuestionInviteUserInfo(ctx *gin.Context) {
|
||||
questionID := uid.DeShortID(ctx.Query("id"))
|
||||
resp, err := qc.questionService.InviteUserInfo(ctx, questionID)
|
||||
handler.HandleResponse(ctx, err, resp)
|
||||
|
||||
}
|
||||
|
||||
// SimilarQuestion godoc
|
||||
// @Summary Search Similar Question
|
||||
// @Description Search Similar Question
|
||||
|
@ -500,18 +519,49 @@ func (qc *QuestionController) UpdateQuestion(ctx *gin.Context) {
|
|||
handler.HandleResponse(ctx, nil, &schema.UpdateQuestionResp{WaitForReview: !req.NoNeedReview})
|
||||
}
|
||||
|
||||
// CloseMsgList close question msg list
|
||||
// @Summary close question msg list
|
||||
// @Description close question msg list
|
||||
// UpdateQuestionInviteUser update question invite user
|
||||
// @Summary update question invite user
|
||||
// @Description update question invite user
|
||||
// @Tags Question
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Param data body schema.QuestionUpdateInviteUser true "question"
|
||||
// @Success 200 {object} handler.RespBody
|
||||
// @Router /answer/api/v1/question/closemsglist [get]
|
||||
func (qc *QuestionController) CloseMsgList(ctx *gin.Context) {
|
||||
resp, err := qc.questionService.CloseMsgList(ctx, handler.GetLang(ctx))
|
||||
handler.HandleResponse(ctx, err, resp)
|
||||
// @Router /answer/api/v1/question/invite [put]
|
||||
func (qc *QuestionController) UpdateQuestionInviteUser(ctx *gin.Context) {
|
||||
req := &schema.QuestionUpdateInviteUser{}
|
||||
errFields := handler.BindAndCheckReturnErr(ctx, req)
|
||||
if ctx.IsAborted() {
|
||||
return
|
||||
}
|
||||
req.ID = uid.DeShortID(req.ID)
|
||||
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||
|
||||
canList, err := qc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{
|
||||
permission.AnswerInviteSomeoneToAnswer,
|
||||
})
|
||||
if err != nil {
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
return
|
||||
}
|
||||
|
||||
objectOwner := qc.rankService.CheckOperationObjectOwner(ctx, req.UserID, req.ID)
|
||||
req.CanEdit = canList[0] || objectOwner
|
||||
if !req.CanEdit {
|
||||
handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil)
|
||||
return
|
||||
}
|
||||
if len(errFields) > 0 {
|
||||
handler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), errFields)
|
||||
return
|
||||
}
|
||||
err = qc.questionService.UpdateQuestionInviteUser(ctx, req)
|
||||
if err != nil {
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
return
|
||||
}
|
||||
handler.HandleResponse(ctx, nil, nil)
|
||||
}
|
||||
|
||||
// SearchByTitleLike add question title like
|
||||
|
|
|
@ -55,21 +55,3 @@ func (rc *ReportController) AddReport(ctx *gin.Context) {
|
|||
err = rc.reportService.AddReport(ctx, req)
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
}
|
||||
|
||||
// GetReportTypeList get report type list
|
||||
// @Summary get report type list
|
||||
// @Description get report type list
|
||||
// @Tags Report
|
||||
// @Produce json
|
||||
// @Param source query string true "report source" Enums(question, answer, comment, user)
|
||||
// @Success 200 {object} handler.RespBody{data=[]schema.GetReportTypeResp}
|
||||
// @Router /answer/api/v1/report/type/list [get]
|
||||
func (rc *ReportController) GetReportTypeList(ctx *gin.Context) {
|
||||
req := &schema.GetReportListReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := rc.reportService.GetReportTypeList(ctx, handler.GetLang(ctx), req)
|
||||
handler.HandleResponse(ctx, err, resp)
|
||||
}
|
||||
|
|
|
@ -470,6 +470,8 @@ func (tc *TemplateController) html(ctx *gin.Context, code int, tpl string, siteI
|
|||
data["description"] = siteInfo.Description
|
||||
data["language"] = handler.GetLang(ctx)
|
||||
data["timezone"] = siteInfo.Interface.TimeZone
|
||||
language := strings.Replace(siteInfo.Interface.Language, "_", "-", -1)
|
||||
data["lang"] = language
|
||||
data["HeadCode"] = siteInfo.CustomCssHtml.CustomHead
|
||||
data["HeaderCode"] = siteInfo.CustomCssHtml.CustomHeader
|
||||
data["FooterCode"] = siteInfo.CustomCssHtml.CustomFooter
|
||||
|
|
|
@ -350,6 +350,21 @@ func (uc *UserController) UserModifyPassWord(ctx *gin.Context) {
|
|||
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||
req.AccessToken = middleware.ExtractToken(ctx)
|
||||
|
||||
captchaPass := uc.actionService.ActionRecordVerifyCaptcha(ctx, schema.ActionRecordTypeModifyPass, ctx.ClientIP(),
|
||||
req.CaptchaID, req.CaptchaCode)
|
||||
if !captchaPass {
|
||||
errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{
|
||||
ErrorField: "captcha_code",
|
||||
ErrorMsg: translator.Tr(handler.GetLang(ctx), reason.CaptchaVerificationFailed),
|
||||
})
|
||||
handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields)
|
||||
return
|
||||
}
|
||||
_, err := uc.actionService.ActionRecordAdd(ctx, schema.ActionRecordTypeModifyPass, ctx.ClientIP())
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
|
||||
oldPassVerification, err := uc.userService.UserModifyPassWordVerification(ctx, req)
|
||||
if err != nil {
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
|
@ -363,6 +378,7 @@ func (uc *UserController) UserModifyPassWord(ctx *gin.Context) {
|
|||
handler.HandleResponse(ctx, errors.BadRequest(reason.OldPasswordVerificationFailed), errFields)
|
||||
return
|
||||
}
|
||||
|
||||
if req.OldPass == req.Pass {
|
||||
errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{
|
||||
ErrorField: "pass",
|
||||
|
@ -372,6 +388,9 @@ func (uc *UserController) UserModifyPassWord(ctx *gin.Context) {
|
|||
return
|
||||
}
|
||||
err = uc.userService.UserModifyPassword(ctx, req)
|
||||
if err == nil {
|
||||
uc.actionService.ActionRecordDel(ctx, schema.ActionRecordTypeLogin, ctx.ClientIP())
|
||||
}
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
}
|
||||
|
||||
|
@ -588,3 +607,22 @@ func (uc *UserController) UserUnsubscribeEmailNotification(ctx *gin.Context) {
|
|||
err := uc.userService.UserUnsubscribeEmailNotification(ctx, req)
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
}
|
||||
|
||||
// SearchUserListByName godoc
|
||||
// @Summary SearchUserListByName
|
||||
// @Description SearchUserListByName
|
||||
// @Tags User
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Param username query string true "username"
|
||||
// @Success 200 {object} handler.RespBody{data=schema.GetOtherUserInfoResp}
|
||||
// @Router /answer/api/v1/user/info/search [get]
|
||||
func (uc *UserController) SearchUserListByName(ctx *gin.Context) {
|
||||
req := &schema.GetOtherUserInfoByUsernameReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
resp, err := uc.userService.SearchUserListByName(ctx, req.Username)
|
||||
handler.HandleResponse(ctx, err, resp)
|
||||
}
|
||||
|
|
|
@ -32,6 +32,7 @@ type Question struct {
|
|||
CreatedAt time.Time `xorm:"not null default CURRENT_TIMESTAMP TIMESTAMP created_at"`
|
||||
UpdatedAt time.Time `xorm:"updated_at TIMESTAMP"`
|
||||
UserID string `xorm:"not null default 0 BIGINT(20) INDEX user_id"`
|
||||
InviteUserID string `xorm:"TEXT invite_user_id"`
|
||||
LastEditUserID string `xorm:"not null default 0 BIGINT(20) last_edit_user_id"`
|
||||
Title string `xorm:"not null default '' VARCHAR(150) title"`
|
||||
OriginalText string `xorm:"not null MEDIUMTEXT original_text"`
|
||||
|
|
|
@ -4,6 +4,7 @@ import "time"
|
|||
|
||||
const (
|
||||
TagRelStatusAvailable = 1
|
||||
TagRelStatusHide = 2
|
||||
TagRelStatusDeleted = 10
|
||||
)
|
||||
|
||||
|
|
|
@ -182,6 +182,7 @@ func initSiteInfo(engine *xorm.Engine, language, siteName, siteURL, contactEmail
|
|||
|
||||
usersData := map[string]any{
|
||||
"default_avatar": "gravatar",
|
||||
"default_gravatar_base_url": "https://www.gravatar.com/avatar/",
|
||||
"allow_update_display_name": true,
|
||||
"allow_update_username": true,
|
||||
"allow_update_avatar": true,
|
||||
|
@ -374,6 +375,7 @@ func initConfigTable(engine *xorm.Engine) error {
|
|||
{ID: 124, Key: "rank.question.unpin", Value: `-1`},
|
||||
{ID: 125, Key: "rank.question.show", Value: `-1`},
|
||||
{ID: 126, Key: "rank.question.hide", Value: `-1`},
|
||||
{ID: 127, Key: "rank.answer.invite_someone_to_answer", Value: `1000`},
|
||||
}
|
||||
_, err := engine.Insert(defaultConfigTable)
|
||||
return err
|
||||
|
@ -428,6 +430,7 @@ func initRolePower(engine *xorm.Engine) (err error) {
|
|||
{ID: 35, Name: "question hide", PowerType: permission.QuestionHide, Description: "hide the question"},
|
||||
{ID: 36, Name: "question unpin", PowerType: permission.QuestionUnPin, Description: "untop the question"},
|
||||
{ID: 37, Name: "question show", PowerType: permission.QuestionShow, Description: "show the question"},
|
||||
{ID: 38, Name: "invite someone to answer", PowerType: permission.AnswerInviteSomeoneToAnswer, Description: "invite someone to answer"},
|
||||
}
|
||||
_, err = engine.Insert(powers)
|
||||
if err != nil {
|
||||
|
@ -473,6 +476,7 @@ func initRolePower(engine *xorm.Engine) (err error) {
|
|||
{RoleID: 2, PowerType: permission.QuestionHide},
|
||||
{RoleID: 2, PowerType: permission.QuestionUnPin},
|
||||
{RoleID: 2, PowerType: permission.QuestionShow},
|
||||
{RoleID: 2, PowerType: permission.AnswerInviteSomeoneToAnswer},
|
||||
|
||||
{RoleID: 3, PowerType: permission.QuestionAdd},
|
||||
{RoleID: 3, PowerType: permission.QuestionEdit},
|
||||
|
@ -511,6 +515,7 @@ func initRolePower(engine *xorm.Engine) (err error) {
|
|||
{RoleID: 3, PowerType: permission.QuestionHide},
|
||||
{RoleID: 3, PowerType: permission.QuestionUnPin},
|
||||
{RoleID: 3, PowerType: permission.QuestionShow},
|
||||
{RoleID: 3, PowerType: permission.AnswerInviteSomeoneToAnswer},
|
||||
}
|
||||
_, err = engine.Insert(rolePowerRels)
|
||||
if err != nil {
|
||||
|
|
|
@ -9,21 +9,28 @@ import (
|
|||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
const minDBVersion = 0 // answer 1.0.0
|
||||
const minDBVersion = 0
|
||||
|
||||
// Migration describes on migration from lower version to high version
|
||||
type Migration interface {
|
||||
Version() string
|
||||
Description() string
|
||||
Migrate(*xorm.Engine) error
|
||||
ShouldCleanCache() bool
|
||||
}
|
||||
|
||||
type migration struct {
|
||||
version string
|
||||
description string
|
||||
migrate func(*xorm.Engine) error
|
||||
shouldCleanCache bool
|
||||
}
|
||||
|
||||
// Version returns the migration's version
|
||||
func (m *migration) Version() string {
|
||||
return m.version
|
||||
}
|
||||
|
||||
// Description returns the migration's description
|
||||
func (m *migration) Description() string {
|
||||
return m.description
|
||||
|
@ -40,8 +47,8 @@ func (m *migration) ShouldCleanCache() bool {
|
|||
}
|
||||
|
||||
// NewMigration creates a new migration
|
||||
func NewMigration(desc string, fn func(*xorm.Engine) error, shouldCleanCache bool) Migration {
|
||||
return &migration{description: desc, migrate: fn, shouldCleanCache: shouldCleanCache}
|
||||
func NewMigration(version, desc string, fn func(*xorm.Engine) error, shouldCleanCache bool) Migration {
|
||||
return &migration{version: version, description: desc, migrate: fn, shouldCleanCache: shouldCleanCache}
|
||||
}
|
||||
|
||||
// Use noopMigration when there is a migration that has been no-oped
|
||||
|
@ -49,19 +56,20 @@ var noopMigration = func(_ *xorm.Engine) error { return nil }
|
|||
|
||||
var migrations = []Migration{
|
||||
// 0->1
|
||||
NewMigration("this is first version, no operation", noopMigration, false),
|
||||
NewMigration("add user language", addUserLanguage, false),
|
||||
NewMigration("add recommend and reserved tag fields", addTagRecommendedAndReserved, false),
|
||||
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),
|
||||
NewMigration("add user pin hide features", addRolePinAndHideFeatures, true),
|
||||
NewMigration("update accept answer rank", updateAcceptAnswerRank, true),
|
||||
NewMigration("add plugin", addPlugin, false),
|
||||
NewMigration("update user pin hide features", updateRolePinAndHideFeatures, true),
|
||||
NewMigration("update question post time", updateQuestionPostTime, true),
|
||||
NewMigration("add login limitations", addLoginLimitations, true),
|
||||
NewMigration("v0.0.1", "this is first version, no operation", noopMigration, false),
|
||||
NewMigration("v0.3.0", "add user language", addUserLanguage, false),
|
||||
NewMigration("v0.4.1", "add recommend and reserved tag fields", addTagRecommendedAndReserved, false),
|
||||
NewMigration("v0.5.0", "add activity timeline", addActivityTimeline, false),
|
||||
NewMigration("v0.6.0", "add user role", addRoleFeatures, false),
|
||||
NewMigration("v1.0.0", "add theme and private mode", addThemeAndPrivateMode, true),
|
||||
NewMigration("v1.0.2", "add new answer notification", addNewAnswerNotification, true),
|
||||
NewMigration("v1.0.5", "add plugin", addPlugin, false),
|
||||
NewMigration("v1.0.7", "add user pin hide features", addRolePinAndHideFeatures, true),
|
||||
NewMigration("v1.0.8", "update accept answer rank", updateAcceptAnswerRank, true),
|
||||
NewMigration("v1.0.9", "add login limitations", addLoginLimitations, true),
|
||||
NewMigration("v1.1.0-beta.1", "update user pin hide features", updateRolePinAndHideFeatures, true),
|
||||
NewMigration("v1.1.0-beta.2", "update question post time", updateQuestionPostTime, true),
|
||||
NewMigration("v1.1.0", "add gravatar base url", updateCount, false),
|
||||
}
|
||||
|
||||
// GetCurrentDBVersion returns the current db version
|
||||
|
@ -91,7 +99,7 @@ func ExpectedVersion() int64 {
|
|||
}
|
||||
|
||||
// Migrate database to current version
|
||||
func Migrate(dbConf *data.Database, cacheConf *data.CacheConf) error {
|
||||
func Migrate(dbConf *data.Database, cacheConf *data.CacheConf, upgradeToSpecificVersion string) error {
|
||||
cache, cacheCleanup, err := data.NewCache(cacheConf)
|
||||
if err != nil {
|
||||
fmt.Println("new check failed:", err.Error())
|
||||
|
@ -107,12 +115,21 @@ func Migrate(dbConf *data.Database, cacheConf *data.CacheConf) error {
|
|||
return err
|
||||
}
|
||||
expectedVersion := ExpectedVersion()
|
||||
if len(upgradeToSpecificVersion) > 0 {
|
||||
fmt.Printf("[migrate] user set upgrade to version: %s\n", upgradeToSpecificVersion)
|
||||
for i, m := range migrations {
|
||||
if m.Version() == upgradeToSpecificVersion {
|
||||
currentDBVersion = int64(i)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for currentDBVersion < expectedVersion {
|
||||
fmt.Printf("[migrate] current db version is %d, try to migrate version %d, latest version is %d\n",
|
||||
currentDBVersion, currentDBVersion+1, expectedVersion)
|
||||
migrationFunc := migrations[currentDBVersion]
|
||||
fmt.Printf("[migrate] try to migrate db version %d, description: %s\n", currentDBVersion+1, migrationFunc.Description())
|
||||
fmt.Printf("[migrate] try to migrate Answer version %s, description: %s\n", migrationFunc.Version(), migrationFunc.Description())
|
||||
if err := migrationFunc.Migrate(engine); err != nil {
|
||||
fmt.Printf("[migrate] migrate to db version %d failed: %s\n", currentDBVersion+1, err.Error())
|
||||
return err
|
||||
|
|
|
@ -24,6 +24,8 @@ func addLoginLimitations(x *xorm.Engine) error {
|
|||
_ = json.Unmarshal([]byte(loginSiteInfo.Content), content)
|
||||
content.AllowEmailRegistrations = true
|
||||
content.AllowEmailDomains = make([]string, 0)
|
||||
data, _ := json.Marshal(content)
|
||||
loginSiteInfo.Content = string(data)
|
||||
_, err = x.ID(loginSiteInfo.ID).Cols("content").Update(loginSiteInfo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("update site info failed: %w", err)
|
||||
|
|
|
@ -2,14 +2,44 @@ package migrations
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/answerdev/answer/internal/entity"
|
||||
"github.com/segmentfault/pacman/log"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
type QuestionPostTime struct {
|
||||
ID string `xorm:"not null pk BIGINT(20) id"`
|
||||
CreatedAt time.Time `xorm:"not null default CURRENT_TIMESTAMP TIMESTAMP created_at"`
|
||||
UpdatedAt time.Time `xorm:"updated_at TIMESTAMP"`
|
||||
UserID string `xorm:"not null default 0 BIGINT(20) INDEX user_id"`
|
||||
InviteUserID string `xorm:"TEXT invite_user_id"`
|
||||
LastEditUserID string `xorm:"not null default 0 BIGINT(20) last_edit_user_id"`
|
||||
Title string `xorm:"not null default '' VARCHAR(150) title"`
|
||||
OriginalText string `xorm:"not null MEDIUMTEXT original_text"`
|
||||
ParsedText string `xorm:"not null MEDIUMTEXT parsed_text"`
|
||||
Status int `xorm:"not null default 1 INT(11) status"`
|
||||
Pin int `xorm:"not null default 1 INT(11) pin"`
|
||||
Show int `xorm:"not null default 1 INT(11) show"`
|
||||
ViewCount int `xorm:"not null default 0 INT(11) view_count"`
|
||||
UniqueViewCount int `xorm:"not null default 0 INT(11) unique_view_count"`
|
||||
VoteCount int `xorm:"not null default 0 INT(11) vote_count"`
|
||||
AnswerCount int `xorm:"not null default 0 INT(11) answer_count"`
|
||||
CollectionCount int `xorm:"not null default 0 INT(11) collection_count"`
|
||||
FollowCount int `xorm:"not null default 0 INT(11) follow_count"`
|
||||
AcceptedAnswerID string `xorm:"not null default 0 BIGINT(20) accepted_answer_id"`
|
||||
LastAnswerID string `xorm:"not null default 0 BIGINT(20) last_answer_id"`
|
||||
PostUpdateTime time.Time `xorm:"post_update_time TIMESTAMP"`
|
||||
RevisionID string `xorm:"not null default 0 BIGINT(20) revision_id"`
|
||||
}
|
||||
|
||||
func (QuestionPostTime) TableName() string {
|
||||
return "question"
|
||||
}
|
||||
|
||||
func updateQuestionPostTime(x *xorm.Engine) error {
|
||||
questionList := make([]entity.Question, 0)
|
||||
questionList := make([]QuestionPostTime, 0)
|
||||
err := x.Find(&questionList, &entity.Question{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("get questions failed: %w", err)
|
||||
|
@ -21,7 +51,7 @@ func updateQuestionPostTime(x *xorm.Engine) error {
|
|||
} else if !item.CreatedAt.IsZero() {
|
||||
item.PostUpdateTime = item.CreatedAt
|
||||
}
|
||||
if _, err = x.Update(item, &entity.Question{ID: item.ID}); err != nil {
|
||||
if _, err = x.Update(item, &QuestionPostTime{ID: item.ID}); err != nil {
|
||||
log.Errorf("update %+v config failed: %s", item, err)
|
||||
return fmt.Errorf("update question failed: %w", err)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,339 @@
|
|||
package migrations
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/answerdev/answer/internal/base/constant"
|
||||
"github.com/answerdev/answer/internal/entity"
|
||||
"github.com/answerdev/answer/internal/schema"
|
||||
"github.com/answerdev/answer/internal/service/permission"
|
||||
"github.com/segmentfault/pacman/log"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
func updateCount(x *xorm.Engine) error {
|
||||
addPrivilegeForInviteSomeoneToAnswer(x)
|
||||
addGravatarBaseURL(x)
|
||||
updateQuestionCount(x)
|
||||
updateTagCount(x)
|
||||
updateUserQuestionCount(x)
|
||||
updateUserAnswerCount(x)
|
||||
inviteAnswer(x)
|
||||
return nil
|
||||
}
|
||||
|
||||
func addGravatarBaseURL(x *xorm.Engine) error {
|
||||
usersSiteInfo := &entity.SiteInfo{
|
||||
Type: constant.SiteTypeUsers,
|
||||
}
|
||||
exist, err := x.Get(usersSiteInfo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("get config failed: %w", err)
|
||||
}
|
||||
if exist {
|
||||
content := &schema.SiteUsersReq{}
|
||||
_ = json.Unmarshal([]byte(usersSiteInfo.Content), content)
|
||||
content.GravatarBaseURL = "https://www.gravatar.com/avatar/"
|
||||
data, _ := json.Marshal(content)
|
||||
usersSiteInfo.Content = string(data)
|
||||
|
||||
_, err = x.ID(usersSiteInfo.ID).Cols("content").Update(usersSiteInfo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("update site info failed: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func addPrivilegeForInviteSomeoneToAnswer(x *xorm.Engine) error {
|
||||
// add rank for invite to answer
|
||||
powers := []*entity.Power{
|
||||
{ID: 38, Name: "invite someone to answer", PowerType: permission.AnswerInviteSomeoneToAnswer, Description: "invite someone to answer"},
|
||||
}
|
||||
for _, power := range powers {
|
||||
exist, err := x.Get(&entity.Power{PowerType: power.PowerType})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if exist {
|
||||
_, err = x.ID(power.ID).Update(power)
|
||||
} else {
|
||||
_, err = x.Insert(power)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
rolePowerRels := []*entity.RolePowerRel{
|
||||
{RoleID: 2, PowerType: permission.AnswerInviteSomeoneToAnswer},
|
||||
{RoleID: 3, PowerType: permission.AnswerInviteSomeoneToAnswer},
|
||||
}
|
||||
for _, rel := range rolePowerRels {
|
||||
exist, err := x.Get(&entity.RolePowerRel{RoleID: rel.RoleID, PowerType: rel.PowerType})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if exist {
|
||||
continue
|
||||
}
|
||||
_, err = x.Insert(rel)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
defaultConfigTable := []*entity.Config{
|
||||
{ID: 127, Key: "rank.answer.invite_someone_to_answer", Value: `1000`},
|
||||
}
|
||||
for _, c := range defaultConfigTable {
|
||||
exist, err := x.Get(&entity.Config{ID: c.ID})
|
||||
if err != nil {
|
||||
return fmt.Errorf("get config failed: %w", err)
|
||||
}
|
||||
if exist {
|
||||
if _, err = x.Update(c, &entity.Config{ID: c.ID}); err != nil {
|
||||
return fmt.Errorf("update config failed: %w", err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if _, err = x.Insert(&entity.Config{ID: c.ID, Key: c.Key, Value: c.Value}); err != nil {
|
||||
return fmt.Errorf("add config failed: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func updateQuestionCount(x *xorm.Engine) error {
|
||||
//question answer count
|
||||
answers := make([]entity.Answer, 0)
|
||||
err := x.Find(&answers, &entity.Answer{Status: entity.AnswerStatusAvailable})
|
||||
if err != nil {
|
||||
return fmt.Errorf("get answers failed: %w", err)
|
||||
}
|
||||
questionAnswerCount := make(map[string]int)
|
||||
for _, answer := range answers {
|
||||
_, ok := questionAnswerCount[answer.QuestionID]
|
||||
if !ok {
|
||||
questionAnswerCount[answer.QuestionID] = 1
|
||||
} else {
|
||||
questionAnswerCount[answer.QuestionID]++
|
||||
}
|
||||
}
|
||||
questionList := make([]entity.Question, 0)
|
||||
err = x.Find(&questionList, &entity.Question{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("get questions failed: %w", err)
|
||||
}
|
||||
for _, item := range questionList {
|
||||
_, ok := questionAnswerCount[item.ID]
|
||||
if ok {
|
||||
item.AnswerCount = questionAnswerCount[item.ID]
|
||||
if _, err = x.Update(item, &entity.Question{ID: item.ID}); err != nil {
|
||||
log.Errorf("update %+v config failed: %s", item, err)
|
||||
return fmt.Errorf("update question failed: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// updateTagCount update tag count
|
||||
func updateTagCount(x *xorm.Engine) error {
|
||||
tagRelList := make([]entity.TagRel, 0)
|
||||
err := x.Find(&tagRelList, &entity.TagRel{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("get tag rel failed: %w", err)
|
||||
}
|
||||
questionIDs := make([]string, 0)
|
||||
questionsAvailableMap := make(map[string]bool)
|
||||
questionsHideMap := make(map[string]bool)
|
||||
for _, item := range tagRelList {
|
||||
questionIDs = append(questionIDs, item.ObjectID)
|
||||
questionsAvailableMap[item.ObjectID] = false
|
||||
questionsHideMap[item.ObjectID] = false
|
||||
}
|
||||
questionList := make([]entity.Question, 0)
|
||||
err = x.In("id", questionIDs).In("question.status", []int{entity.QuestionStatusAvailable, entity.QuestionStatusClosed}).Find(&questionList, &entity.Question{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("get questions failed: %w", err)
|
||||
}
|
||||
for _, question := range questionList {
|
||||
_, ok := questionsAvailableMap[question.ID]
|
||||
if ok {
|
||||
questionsAvailableMap[question.ID] = true
|
||||
if question.Show == entity.QuestionHide {
|
||||
questionsHideMap[question.ID] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for id, ok := range questionsHideMap {
|
||||
if ok {
|
||||
if _, err = x.Cols("status").Update(&entity.TagRel{Status: entity.TagRelStatusHide}, &entity.TagRel{ObjectID: id}); err != nil {
|
||||
log.Errorf("update %+v config failed: %s", id, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for id, ok := range questionsAvailableMap {
|
||||
if !ok {
|
||||
if _, err = x.Cols("status").Update(&entity.TagRel{Status: entity.TagRelStatusDeleted}, &entity.TagRel{ObjectID: id}); err != nil {
|
||||
log.Errorf("update %+v config failed: %s", id, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//select tag count
|
||||
newTagRelList := make([]entity.TagRel, 0)
|
||||
err = x.Find(&newTagRelList, &entity.TagRel{Status: entity.TagRelStatusAvailable})
|
||||
if err != nil {
|
||||
return fmt.Errorf("get tag rel failed: %w", err)
|
||||
}
|
||||
tagCountMap := make(map[string]int)
|
||||
for _, v := range newTagRelList {
|
||||
_, ok := tagCountMap[v.TagID]
|
||||
if !ok {
|
||||
tagCountMap[v.TagID] = 1
|
||||
} else {
|
||||
tagCountMap[v.TagID]++
|
||||
}
|
||||
}
|
||||
TagList := make([]entity.Tag, 0)
|
||||
err = x.Find(&TagList, &entity.Tag{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("get tag failed: %w", err)
|
||||
}
|
||||
for _, tag := range TagList {
|
||||
_, ok := tagCountMap[tag.ID]
|
||||
if ok {
|
||||
tag.QuestionCount = tagCountMap[tag.ID]
|
||||
if _, err = x.Update(tag, &entity.Tag{ID: tag.ID}); err != nil {
|
||||
log.Errorf("update %+v tag failed: %s", tag.ID, err)
|
||||
return fmt.Errorf("update tag failed: %w", err)
|
||||
}
|
||||
} else {
|
||||
tag.QuestionCount = 0
|
||||
if _, err = x.Update(tag, &entity.Tag{ID: tag.ID}); err != nil {
|
||||
log.Errorf("update %+v tag failed: %s", tag.ID, err)
|
||||
return fmt.Errorf("update tag failed: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// updateUserQuestionCount update user question count
|
||||
func updateUserQuestionCount(x *xorm.Engine) error {
|
||||
questionList := make([]entity.Question, 0)
|
||||
err := x.In("status", []int{entity.QuestionStatusAvailable, entity.QuestionStatusClosed}).Find(&questionList, &entity.Question{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("get question failed: %w", err)
|
||||
}
|
||||
userQuestionCountMap := make(map[string]int)
|
||||
for _, question := range questionList {
|
||||
_, ok := userQuestionCountMap[question.UserID]
|
||||
if !ok {
|
||||
userQuestionCountMap[question.UserID] = 1
|
||||
} else {
|
||||
userQuestionCountMap[question.UserID]++
|
||||
}
|
||||
}
|
||||
userList := make([]entity.User, 0)
|
||||
err = x.Find(&userList, &entity.User{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("get user failed: %w", err)
|
||||
}
|
||||
for _, user := range userList {
|
||||
_, ok := userQuestionCountMap[user.ID]
|
||||
if ok {
|
||||
user.QuestionCount = userQuestionCountMap[user.ID]
|
||||
if _, err = x.Cols("question_count").Update(user, &entity.User{ID: user.ID}); err != nil {
|
||||
log.Errorf("update %+v user failed: %s", user.ID, err)
|
||||
return fmt.Errorf("update user failed: %w", err)
|
||||
}
|
||||
} else {
|
||||
user.QuestionCount = 0
|
||||
if _, err = x.Cols("question_count").Update(user, &entity.User{ID: user.ID}); err != nil {
|
||||
log.Errorf("update %+v user failed: %s", user.ID, err)
|
||||
return fmt.Errorf("update user failed: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// updateUserAnswerCount update user answer count
|
||||
func updateUserAnswerCount(x *xorm.Engine) error {
|
||||
answers := make([]entity.Answer, 0)
|
||||
err := x.Find(&answers, &entity.Answer{Status: entity.AnswerStatusAvailable})
|
||||
if err != nil {
|
||||
return fmt.Errorf("get answers failed: %w", err)
|
||||
}
|
||||
userAnswerCount := make(map[string]int)
|
||||
for _, answer := range answers {
|
||||
_, ok := userAnswerCount[answer.UserID]
|
||||
if !ok {
|
||||
userAnswerCount[answer.UserID] = 1
|
||||
} else {
|
||||
userAnswerCount[answer.UserID]++
|
||||
}
|
||||
}
|
||||
userList := make([]entity.User, 0)
|
||||
err = x.Find(&userList, &entity.User{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("get user failed: %w", err)
|
||||
}
|
||||
for _, user := range userList {
|
||||
_, ok := userAnswerCount[user.ID]
|
||||
if ok {
|
||||
user.AnswerCount = userAnswerCount[user.ID]
|
||||
if _, err = x.Cols("answer_count").Update(user, &entity.User{ID: user.ID}); err != nil {
|
||||
log.Errorf("update %+v user failed: %s", user.ID, err)
|
||||
return fmt.Errorf("update user failed: %w", err)
|
||||
}
|
||||
} else {
|
||||
user.AnswerCount = 0
|
||||
if _, err = x.Cols("answer_count").Update(user, &entity.User{ID: user.ID}); err != nil {
|
||||
log.Errorf("update %+v user failed: %s", user.ID, err)
|
||||
return fmt.Errorf("update user failed: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func inviteAnswer(x *xorm.Engine) error {
|
||||
type Question struct {
|
||||
ID string `xorm:"not null pk BIGINT(20) id"`
|
||||
CreatedAt time.Time `xorm:"not null default CURRENT_TIMESTAMP TIMESTAMP created_at"`
|
||||
UpdatedAt time.Time `xorm:"updated_at TIMESTAMP"`
|
||||
UserID string `xorm:"not null default 0 BIGINT(20) INDEX user_id"`
|
||||
InviteUserID string `xorm:"TEXT invite_user_id"`
|
||||
LastEditUserID string `xorm:"not null default 0 BIGINT(20) last_edit_user_id"`
|
||||
Title string `xorm:"not null default '' VARCHAR(150) title"`
|
||||
OriginalText string `xorm:"not null MEDIUMTEXT original_text"`
|
||||
ParsedText string `xorm:"not null MEDIUMTEXT parsed_text"`
|
||||
Status int `xorm:"not null default 1 INT(11) status"`
|
||||
Pin int `xorm:"not null default 1 INT(11) pin"`
|
||||
Show int `xorm:"not null default 1 INT(11) show"`
|
||||
ViewCount int `xorm:"not null default 0 INT(11) view_count"`
|
||||
UniqueViewCount int `xorm:"not null default 0 INT(11) unique_view_count"`
|
||||
VoteCount int `xorm:"not null default 0 INT(11) vote_count"`
|
||||
AnswerCount int `xorm:"not null default 0 INT(11) answer_count"`
|
||||
CollectionCount int `xorm:"not null default 0 INT(11) collection_count"`
|
||||
FollowCount int `xorm:"not null default 0 INT(11) follow_count"`
|
||||
AcceptedAnswerID string `xorm:"not null default 0 BIGINT(20) accepted_answer_id"`
|
||||
LastAnswerID string `xorm:"not null default 0 BIGINT(20) last_answer_id"`
|
||||
PostUpdateTime time.Time `xorm:"post_update_time TIMESTAMP"`
|
||||
RevisionID string `xorm:"not null default 0 BIGINT(20) revision_id"`
|
||||
}
|
||||
err := x.Sync(new(Question))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -219,6 +219,24 @@ func (qr *questionRepo) GetQuestionCount(ctx context.Context) (count int64, err
|
|||
return
|
||||
}
|
||||
|
||||
func (qr *questionRepo) GetUserQuestionCount(ctx context.Context, userID string) (count int64, err error) {
|
||||
questionList := make([]*entity.Question, 0)
|
||||
count, err = qr.data.DB.In("question.status", []int{entity.QuestionStatusAvailable, entity.QuestionStatusClosed}).And("user_id = ?", userID).Count(&questionList)
|
||||
if err != nil {
|
||||
return count, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (qr *questionRepo) GetQuestionCountByIDs(ctx context.Context, ids []string) (count int64, err error) {
|
||||
questionList := make([]*entity.Question, 0)
|
||||
count, err = qr.data.DB.In("question.status", []int{entity.QuestionStatusAvailable, entity.QuestionStatusClosed}).In("id = ?", ids).Count(&questionList)
|
||||
if err != nil {
|
||||
return count, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (qr *questionRepo) GetQuestionIDsPage(ctx context.Context, page, pageSize int) (questionIDList []*schema.SiteMapQuestionInfo, err error) {
|
||||
questionIDList = make([]*schema.SiteMapQuestionInfo, 0)
|
||||
rows := make([]*entity.Question, 0)
|
||||
|
|
|
@ -138,7 +138,7 @@ func (ur *UserRankRepo) UserRankPage(ctx context.Context, userID string, page, p
|
|||
) {
|
||||
rankPage = make([]*entity.Activity, 0)
|
||||
|
||||
session := ur.data.DB.Context(ctx).Where(builder.Eq{"has_rank": 1}.And(builder.Eq{"cancelled": 0}))
|
||||
session := ur.data.DB.Context(ctx).Where(builder.Eq{"has_rank": 1}.And(builder.Eq{"cancelled": 0})).And(builder.Gt{"rank": 0})
|
||||
session.Desc("created_at")
|
||||
|
||||
cond := &entity.Activity{UserID: userID}
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/answerdev/answer/internal/base/handler"
|
||||
"github.com/answerdev/answer/internal/schema"
|
||||
"github.com/answerdev/answer/internal/service/config"
|
||||
"github.com/answerdev/answer/internal/service/reason_common"
|
||||
|
@ -21,43 +22,37 @@ func NewReasonRepo(configRepo config.ConfigRepo) reason_common.ReasonRepo {
|
|||
}
|
||||
}
|
||||
|
||||
func (rr *reasonRepo) ListReasons(ctx context.Context, objectType, action string) (resp []schema.ReasonItem, err error) {
|
||||
var (
|
||||
reasonAction = fmt.Sprintf("%s.%s.reasons", objectType, action)
|
||||
reasonKeys []string
|
||||
cfgValue string
|
||||
)
|
||||
resp = []schema.ReasonItem{}
|
||||
func (rr *reasonRepo) ListReasons(ctx context.Context, objectType, action string) (resp []*schema.ReasonItem, err error) {
|
||||
lang := handler.GetLangByCtx(ctx)
|
||||
reasonAction := fmt.Sprintf("%s.%s.reasons", objectType, action)
|
||||
resp = make([]*schema.ReasonItem, 0)
|
||||
|
||||
reasonKeys, err = rr.configRepo.GetArrayString(reasonAction)
|
||||
reasonKeys, err := rr.configRepo.GetArrayString(reasonAction)
|
||||
if err != nil {
|
||||
return
|
||||
return nil, err
|
||||
}
|
||||
for _, reasonKey := range reasonKeys {
|
||||
var (
|
||||
reasonType int
|
||||
reason = schema.ReasonItem{}
|
||||
)
|
||||
|
||||
cfgValue, err = rr.configRepo.GetString(reasonKey)
|
||||
cfgValue, err := rr.configRepo.GetString(reasonKey)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
continue
|
||||
}
|
||||
|
||||
err = json.Unmarshal([]byte(cfgValue), &reason)
|
||||
reason := &schema.ReasonItem{}
|
||||
err = json.Unmarshal([]byte(cfgValue), reason)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
continue
|
||||
}
|
||||
reasonType, err = rr.configRepo.GetConfigType(reasonKey)
|
||||
reason.Translate(reasonKey, lang)
|
||||
|
||||
reason.ReasonType, err = rr.configRepo.GetConfigType(reasonKey)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
continue
|
||||
}
|
||||
|
||||
reason.ReasonType = reasonType
|
||||
resp = append(resp, reason)
|
||||
}
|
||||
return
|
||||
return resp, nil
|
||||
}
|
||||
|
|
|
@ -52,6 +52,24 @@ func (tr *tagRelRepo) RemoveTagRelListByObjectID(ctx context.Context, objectID s
|
|||
return
|
||||
}
|
||||
|
||||
func (tr *tagRelRepo) HideTagRelListByObjectID(ctx context.Context, objectID string) (err error) {
|
||||
objectID = uid.DeShortID(objectID)
|
||||
_, err = tr.data.DB.Where("object_id = ?", objectID).Cols("status").Update(&entity.TagRel{Status: entity.TagRelStatusHide})
|
||||
if err != nil {
|
||||
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (tr *tagRelRepo) ShowTagRelListByObjectID(ctx context.Context, objectID string) (err error) {
|
||||
objectID = uid.DeShortID(objectID)
|
||||
_, err = tr.data.DB.Where("object_id = ?", objectID).Cols("status").Update(&entity.TagRel{Status: entity.TagRelStatusAvailable})
|
||||
if err != nil {
|
||||
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// RemoveTagRelListByIDs delete tag list
|
||||
func (tr *tagRelRepo) RemoveTagRelListByIDs(ctx context.Context, ids []int64) (err error) {
|
||||
_, err = tr.data.DB.Context(ctx).In("id", ids).Update(&entity.TagRel{Status: entity.TagRelStatusDeleted})
|
||||
|
@ -90,7 +108,7 @@ func (tr *tagRelRepo) GetObjectTagRelList(ctx context.Context, objectID string)
|
|||
objectID = uid.DeShortID(objectID)
|
||||
tagListList = make([]*entity.TagRel, 0)
|
||||
session := tr.data.DB.Context(ctx).Where("object_id = ?", objectID)
|
||||
session.Where("status = ?", entity.TagRelStatusAvailable)
|
||||
session.In("status", []int{entity.TagRelStatusAvailable, entity.TagRelStatusHide})
|
||||
err = session.Find(&tagListList)
|
||||
if err != nil {
|
||||
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
|
||||
|
|
|
@ -72,6 +72,26 @@ func (ur *userRepo) IncreaseQuestionCount(ctx context.Context, userID string, am
|
|||
return nil
|
||||
}
|
||||
|
||||
func (ur *userRepo) UpdateQuestionCount(ctx context.Context, userID string, count int64) (err error) {
|
||||
user := &entity.User{}
|
||||
user.QuestionCount = int(count)
|
||||
_, err = ur.data.DB.Where("id = ?", userID).Cols("question_count").Update(user)
|
||||
if err != nil {
|
||||
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ur *userRepo) UpdateAnswerCount(ctx context.Context, userID string, count int) (err error) {
|
||||
user := &entity.User{}
|
||||
user.AnswerCount = count
|
||||
_, err = ur.data.DB.Where("id = ?", userID).Cols("answer_count").Update(user)
|
||||
if err != nil {
|
||||
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateLastLoginDate update last login date
|
||||
func (ur *userRepo) UpdateLastLoginDate(ctx context.Context, userID string) (err error) {
|
||||
user := &entity.User{LastLoginDate: time.Now()}
|
||||
|
@ -176,6 +196,17 @@ func (ur *userRepo) GetByUsername(ctx context.Context, username string) (userInf
|
|||
return
|
||||
}
|
||||
|
||||
func (ur *userRepo) GetByUsernames(ctx context.Context, usernames []string) ([]*entity.User, error) {
|
||||
list := make([]*entity.User, 0)
|
||||
err := ur.data.DB.Where("status =?", entity.UserStatusAvailable).In("username", usernames).Find(&list)
|
||||
if err != nil {
|
||||
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
|
||||
return list, err
|
||||
}
|
||||
tryToDecorateUserListFromUserCenter(ctx, ur.data, list)
|
||||
return list, nil
|
||||
}
|
||||
|
||||
// GetByEmail get user by email
|
||||
func (ur *userRepo) GetByEmail(ctx context.Context, email string) (userInfo *entity.User, exist bool, err error) {
|
||||
userInfo = &entity.User{}
|
||||
|
@ -196,6 +227,23 @@ func (ur *userRepo) GetUserCount(ctx context.Context) (count int64, err error) {
|
|||
return
|
||||
}
|
||||
|
||||
func (ur *userRepo) SearchUserListByName(ctx context.Context, name string) (userList []*entity.User, err error) {
|
||||
userList = make([]*entity.User, 0)
|
||||
if name == "" {
|
||||
return userList, nil
|
||||
}
|
||||
session := ur.data.DB.Where("")
|
||||
session.Where("username LIKE LOWER(?) or display_name LIKE ?", name+"%", name+"%").And("status =?", entity.UserStatusAvailable)
|
||||
session.Asc("username")
|
||||
session = session.Limit(5, 0)
|
||||
err = session.OrderBy("id desc").Find(&userList)
|
||||
if err != nil {
|
||||
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
|
||||
}
|
||||
tryToDecorateUserListFromUserCenter(ctx, ur.data, userList)
|
||||
return
|
||||
}
|
||||
|
||||
func tryToDecorateUserInfoFromUserCenter(ctx context.Context, data *data.Data, original *entity.User) (err error) {
|
||||
if original == nil {
|
||||
return nil
|
||||
|
|
|
@ -129,6 +129,7 @@ func (a *AnswerAPIRouter) RegisterUnAuthAnswerAPIRouter(r *gin.RouterGroup) {
|
|||
|
||||
//question
|
||||
r.GET("/question/info", a.questionController.GetQuestion)
|
||||
r.GET("/question/invite", a.questionController.GetQuestionInviteUserInfo)
|
||||
r.GET("/question/page", a.questionController.QuestionPage)
|
||||
r.GET("/question/similar/tag", a.questionController.SimilarQuestion)
|
||||
r.GET("/personal/qa/top", a.questionController.UserTop)
|
||||
|
@ -193,6 +194,7 @@ func (a *AnswerAPIRouter) RegisterAnswerAPIRouter(r *gin.RouterGroup) {
|
|||
r.POST("/question", a.questionController.AddQuestion)
|
||||
r.POST("/question/answer", a.questionController.AddQuestionByAnswer)
|
||||
r.PUT("/question", a.questionController.UpdateQuestion)
|
||||
r.PUT("/question/invite", a.questionController.UpdateQuestionInviteUser)
|
||||
r.DELETE("/question", a.questionController.RemoveQuestion)
|
||||
r.PUT("/question/status", a.questionController.CloseQuestion)
|
||||
r.PUT("/question/operation", a.questionController.OperationQuestion)
|
||||
|
@ -210,6 +212,7 @@ func (a *AnswerAPIRouter) RegisterAnswerAPIRouter(r *gin.RouterGroup) {
|
|||
r.PUT("/user/info", a.userController.UserUpdateInfo)
|
||||
r.PUT("/user/interface", a.userController.UserUpdateInterface)
|
||||
r.POST("/user/notice/set", a.userController.UserNoticeSet)
|
||||
r.GET("/user/info/search", a.userController.SearchUserListByName)
|
||||
|
||||
// vote
|
||||
r.GET("/personal/vote/page", a.voteController.UserVotes)
|
||||
|
|
|
@ -90,6 +90,9 @@ func (a *UIRouter) Register(r *gin.Engine) {
|
|||
if branding.Favicon != "" {
|
||||
c.String(http.StatusOK, htmltext.GetPicByUrl(branding.Favicon))
|
||||
return
|
||||
} else if branding.SquareIcon != "" {
|
||||
c.String(http.StatusOK, htmltext.GetPicByUrl(branding.SquareIcon))
|
||||
return
|
||||
} else {
|
||||
c.Header("content-type", "image/vnd.microsoft.icon")
|
||||
filePath = UIRootFilePath + urlPath
|
||||
|
|
|
@ -47,6 +47,21 @@ type NewAnswerTemplateData struct {
|
|||
UnsubscribeUrl string
|
||||
}
|
||||
|
||||
type NewInviteAnswerTemplateRawData struct {
|
||||
InviterDisplayName string
|
||||
QuestionTitle string
|
||||
QuestionID string
|
||||
UnsubscribeCode string
|
||||
}
|
||||
|
||||
type NewInviteAnswerTemplateData struct {
|
||||
SiteName string
|
||||
DisplayName string
|
||||
QuestionTitle string
|
||||
InviteUrl string
|
||||
UnsubscribeUrl string
|
||||
}
|
||||
|
||||
type NewCommentTemplateRawData struct {
|
||||
CommentUserDisplayName string
|
||||
QuestionTitle string
|
||||
|
|
|
@ -1,12 +1,16 @@
|
|||
package schema
|
||||
|
||||
const (
|
||||
NotificationTypeInbox = 1
|
||||
NotificationTypeAchievement = 2
|
||||
NotificationNotRead = 1
|
||||
NotificationRead = 2
|
||||
NotificationStatusNormal = 1
|
||||
NotificationStatusDelete = 10
|
||||
NotificationTypeInbox = 1
|
||||
NotificationTypeAchievement = 2
|
||||
NotificationNotRead = 1
|
||||
NotificationRead = 2
|
||||
NotificationStatusNormal = 1
|
||||
NotificationStatusDelete = 10
|
||||
NotificationInboxTypeAll = 1
|
||||
NotificationInboxTypePosts = 2
|
||||
NotificationInboxTypeInvites = 3
|
||||
NotificationInboxTypeVotes = 4
|
||||
)
|
||||
|
||||
var NotificationType = map[string]int{
|
||||
|
@ -14,6 +18,13 @@ var NotificationType = map[string]int{
|
|||
"achievement": NotificationTypeAchievement,
|
||||
}
|
||||
|
||||
var NotificationInboxType = map[string]int{
|
||||
"all": NotificationInboxTypeAll,
|
||||
"posts": NotificationInboxTypePosts,
|
||||
"invites": NotificationInboxTypeInvites,
|
||||
"votes": NotificationInboxTypeVotes,
|
||||
}
|
||||
|
||||
type NotificationContent struct {
|
||||
ID string `json:"id"`
|
||||
TriggerUserID string `json:"-"` //show userid
|
||||
|
@ -69,11 +80,13 @@ type RedDot struct {
|
|||
}
|
||||
|
||||
type NotificationSearch struct {
|
||||
Page int `json:"page" form:"page"` //Query number of pages
|
||||
PageSize int `json:"page_size" form:"page_size"` //Search page size
|
||||
Type int `json:"-" form:"-"`
|
||||
TypeStr string `json:"type" form:"type"` // inbox achievement
|
||||
UserID string `json:"-"`
|
||||
Page int `json:"page" form:"page"` //Query number of pages
|
||||
PageSize int `json:"page_size" form:"page_size"` //Search page size
|
||||
Type int `json:"-" form:"-"`
|
||||
TypeStr string `json:"type" form:"type"` // inbox achievement
|
||||
InboxTypeStr string `json:"inbox_type" form:"inbox_type"` // inbox achievement
|
||||
InboxType int `json:"-" form:"-"` // inbox achievement
|
||||
UserID string `json:"-"`
|
||||
}
|
||||
|
||||
type NotificationClearRequest struct {
|
||||
|
|
|
@ -87,7 +87,8 @@ type QuestionAddByAnswer struct {
|
|||
// tags
|
||||
Tags []*TagItem `validate:"required,dive" json:"tags"`
|
||||
// user id
|
||||
UserID string `json:"-"`
|
||||
UserID string `json:"-"`
|
||||
MentionUsernameList []string `validate:"omitempty" json:"mention_username_list"`
|
||||
QuestionPermission
|
||||
}
|
||||
|
||||
|
@ -121,6 +122,8 @@ type QuestionPermission struct {
|
|||
CanShow bool `json:"-"`
|
||||
// whether user can use reserved it
|
||||
CanUseReservedTag bool `json:"-"`
|
||||
// whether user can invite other user to answer this question
|
||||
CanInviteOtherToAnswer bool `json:"-"`
|
||||
}
|
||||
|
||||
type CheckCanQuestionUpdate struct {
|
||||
|
@ -139,7 +142,8 @@ type QuestionUpdate struct {
|
|||
// content
|
||||
Content string `validate:"required,notblank,gte=6,lte=65535" json:"content"`
|
||||
// html
|
||||
HTML string `json:"-"`
|
||||
HTML string `json:"-"`
|
||||
InviteUser []string `validate:"omitempty" json:"invite_user"`
|
||||
// tags
|
||||
Tags []*TagItem `validate:"required,dive" json:"tags"`
|
||||
// edit summary
|
||||
|
@ -150,6 +154,13 @@ type QuestionUpdate struct {
|
|||
QuestionPermission
|
||||
}
|
||||
|
||||
type QuestionUpdateInviteUser struct {
|
||||
ID string `validate:"required" json:"id"`
|
||||
InviteUser []string `validate:"omitempty" json:"invite_user"`
|
||||
UserID string `json:"-"`
|
||||
QuestionPermission
|
||||
}
|
||||
|
||||
func (req *QuestionUpdate) Check() (errFields []*validator.FormErrorField, err error) {
|
||||
req.HTML = converter.Markdown2HTML(req.Content)
|
||||
return nil, nil
|
||||
|
@ -202,7 +213,8 @@ type QuestionInfo struct {
|
|||
IsFollowed bool `json:"is_followed"`
|
||||
|
||||
// MemberActions
|
||||
MemberActions []*PermissionMemberAction `json:"member_actions"`
|
||||
MemberActions []*PermissionMemberAction `json:"member_actions"`
|
||||
ExtendsActions []*PermissionMemberAction `json:"extends_actions"`
|
||||
}
|
||||
|
||||
// UpdateQuestionResp update question resp
|
||||
|
|
|
@ -1,5 +1,10 @@
|
|||
package schema
|
||||
|
||||
import (
|
||||
"github.com/answerdev/answer/internal/base/translator"
|
||||
"github.com/segmentfault/pacman/i18n"
|
||||
)
|
||||
|
||||
type ReasonItem struct {
|
||||
ReasonType int `json:"reason_type"`
|
||||
Name string `json:"name"`
|
||||
|
@ -14,3 +19,24 @@ type ReasonReq struct {
|
|||
// Action
|
||||
Action string `validate:"required" form:"action" json:"action"`
|
||||
}
|
||||
|
||||
func (r *ReasonItem) Translate(keyPrefix string, lang i18n.Language) {
|
||||
trField := func(fieldName, fieldData string) string {
|
||||
// If fieldData is empty, means no need to translate
|
||||
if len(fieldData) == 0 {
|
||||
return fieldData
|
||||
}
|
||||
key := keyPrefix + "." + fieldName
|
||||
fieldTr := translator.Tr(lang, key)
|
||||
if fieldTr != key {
|
||||
// If i18n key exists, return i18n value
|
||||
return fieldTr
|
||||
}
|
||||
// If i18n key not exists, return fieldData original value
|
||||
return fieldData + "没翻译"
|
||||
}
|
||||
|
||||
r.Name = trField("name", r.Name)
|
||||
r.Description = trField("desc", r.Description)
|
||||
r.Placeholder = trField("placeholder", r.Placeholder)
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ type SimpleObjectInfo struct {
|
|||
ObjectID string `json:"object_id"`
|
||||
ObjectCreatorUserID string `json:"object_creator_user_id"`
|
||||
QuestionID string `json:"question_id"`
|
||||
QuestionStatus int `json:"status"`
|
||||
AnswerID string `json:"answer_id"`
|
||||
CommentID string `json:"comment_id"`
|
||||
TagID string `json:"tag_id"`
|
||||
|
|
|
@ -94,13 +94,14 @@ type GetSiteLegalInfoResp struct {
|
|||
|
||||
// SiteUsersReq site users config request
|
||||
type SiteUsersReq struct {
|
||||
DefaultAvatar string `validate:"required,oneof=system gravatar" form:"default_avatar" json:"default_avatar"`
|
||||
AllowUpdateDisplayName bool `form:"allow_update_display_name" json:"allow_update_display_name"`
|
||||
AllowUpdateUsername bool `form:"allow_update_username" json:"allow_update_username"`
|
||||
AllowUpdateAvatar bool `form:"allow_update_avatar" json:"allow_update_avatar"`
|
||||
AllowUpdateBio bool `form:"allow_update_bio" json:"allow_update_bio"`
|
||||
AllowUpdateWebsite bool `form:"allow_update_website" json:"allow_update_website"`
|
||||
AllowUpdateLocation bool `form:"allow_update_location" json:"allow_update_location"`
|
||||
DefaultAvatar string `validate:"required,oneof=system gravatar" json:"default_avatar"`
|
||||
GravatarBaseURL string `json:"gravatar_base_url"`
|
||||
AllowUpdateDisplayName bool `json:"allow_update_display_name"`
|
||||
AllowUpdateUsername bool `json:"allow_update_username"`
|
||||
AllowUpdateAvatar bool `json:"allow_update_avatar"`
|
||||
AllowUpdateBio bool `json:"allow_update_bio"`
|
||||
AllowUpdateWebsite bool `json:"allow_update_website"`
|
||||
AllowUpdateLocation bool `json:"allow_update_location"`
|
||||
}
|
||||
|
||||
// SiteLoginReq site login request
|
||||
|
@ -296,6 +297,7 @@ var (
|
|||
constant.RankAnswerVoteUpKey: {1, 1, 15},
|
||||
constant.RankQuestionVoteDownKey: {125, 125, 125},
|
||||
constant.RankAnswerVoteDownKey: {125, 125, 125},
|
||||
constant.RankInviteSomeoneToAnswerKey: {1, 500, 1000},
|
||||
constant.RankTagAddKey: {1, 750, 1500},
|
||||
constant.RankTagEditKey: {1, 50, 100},
|
||||
constant.RankQuestionEditKey: {1, 100, 200},
|
||||
|
|
|
@ -222,9 +222,10 @@ const (
|
|||
NoticeStatusOn = 1
|
||||
NoticeStatusOff = 2
|
||||
|
||||
ActionRecordTypeLogin = "login"
|
||||
ActionRecordTypeEmail = "e_mail"
|
||||
ActionRecordTypeFindPass = "find_pass"
|
||||
ActionRecordTypeLogin = "login"
|
||||
ActionRecordTypeEmail = "e_mail"
|
||||
ActionRecordTypeFindPass = "find_pass"
|
||||
ActionRecordTypeModifyPass = "modify_pass"
|
||||
)
|
||||
|
||||
var UserStatusShow = map[int]string{
|
||||
|
@ -262,35 +263,31 @@ type UserRegisterReq struct {
|
|||
}
|
||||
|
||||
func (u *UserRegisterReq) Check() (errFields []*validator.FormErrorField, err error) {
|
||||
// TODO i18n
|
||||
err = checker.CheckPassword(8, 32, 0, u.Pass)
|
||||
if err != nil {
|
||||
errField := &validator.FormErrorField{
|
||||
if err = checker.CheckPassword(u.Pass); err != nil {
|
||||
errFields = append(errFields, &validator.FormErrorField{
|
||||
ErrorField: "pass",
|
||||
ErrorMsg: err.Error(),
|
||||
}
|
||||
errFields = append(errFields, errField)
|
||||
})
|
||||
return errFields, err
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
type UserModifyPasswordReq struct {
|
||||
OldPass string `validate:"omitempty,gte=8,lte=32" json:"old_pass"`
|
||||
Pass string `validate:"required,gte=8,lte=32" json:"pass"`
|
||||
OldPass string `validate:"omitempty,gte=8,lte=32" json:"old_pass"`
|
||||
Pass string `validate:"required,gte=8,lte=32" json:"pass"`
|
||||
UserID string `json:"-"`
|
||||
AccessToken string `json:"-"`
|
||||
CaptchaID string `validate:"omitempty,gt=0,lte=500" json:"captcha_id"`
|
||||
CaptchaCode string `validate:"omitempty,gt=0,lte=500" json:"captcha_code"`
|
||||
}
|
||||
|
||||
func (u *UserModifyPasswordReq) Check() (errFields []*validator.FormErrorField, err error) {
|
||||
// TODO i18n
|
||||
err = checker.CheckPassword(8, 32, 0, u.Pass)
|
||||
if err != nil {
|
||||
errField := &validator.FormErrorField{
|
||||
if err = checker.CheckPassword(u.Pass); err != nil {
|
||||
errFields = append(errFields, &validator.FormErrorField{
|
||||
ErrorField: "pass",
|
||||
ErrorMsg: err.Error(),
|
||||
}
|
||||
errFields = append(errFields, errField)
|
||||
})
|
||||
return errFields, err
|
||||
}
|
||||
return nil, nil
|
||||
|
@ -352,14 +349,11 @@ type UserRePassWordRequest struct {
|
|||
}
|
||||
|
||||
func (u *UserRePassWordRequest) Check() (errFields []*validator.FormErrorField, err error) {
|
||||
// TODO i18n
|
||||
err = checker.CheckPassword(8, 32, 0, u.Pass)
|
||||
if err != nil {
|
||||
errField := &validator.FormErrorField{
|
||||
if err = checker.CheckPassword(u.Pass); err != nil {
|
||||
errFields = append(errFields, &validator.FormErrorField{
|
||||
ErrorField: "pass",
|
||||
ErrorMsg: err.Error(),
|
||||
}
|
||||
errFields = append(errFields, errField)
|
||||
})
|
||||
return errFields, err
|
||||
}
|
||||
return nil, nil
|
||||
|
@ -376,7 +370,7 @@ type UserNoticeSetResp struct {
|
|||
|
||||
type ActionRecordReq struct {
|
||||
// action
|
||||
Action string `validate:"required,oneof=login e_mail find_pass" form:"action"`
|
||||
Action string `validate:"required,oneof=login e_mail find_pass modify_pass" form:"action"`
|
||||
IP string `json:"-"`
|
||||
}
|
||||
|
||||
|
|
|
@ -1,14 +1,22 @@
|
|||
package activity_type
|
||||
|
||||
import "github.com/answerdev/answer/internal/repo/config"
|
||||
import (
|
||||
"github.com/answerdev/answer/internal/repo/config"
|
||||
)
|
||||
|
||||
const (
|
||||
QuestionVoteUp = "question.vote_up"
|
||||
QuestionVoteDown = "question.vote_down"
|
||||
AnswerVoteUp = "answer.vote_up"
|
||||
AnswerVoteDown = "answer.vote_down"
|
||||
CommentVoteUp = "comment.vote_up"
|
||||
CommentVoteDown = "comment.vote_down"
|
||||
QuestionVoteUp = "question.vote_up"
|
||||
QuestionVoteDown = "question.vote_down"
|
||||
AnswerVoteUp = "answer.vote_up"
|
||||
AnswerVoteDown = "answer.vote_down"
|
||||
CommentVoteUp = "comment.vote_up"
|
||||
CommentVoteDown = "comment.vote_down"
|
||||
AnswerAccepted = "answer.accepted"
|
||||
AnswerAccept = "answer.accept"
|
||||
QuestionVotedUp = "question.voted_up"
|
||||
QuestionVotedDown = "question.voted_down"
|
||||
AnswerVotedUp = "answer.voted_up"
|
||||
AnswerVotedDown = "answer.voted_down"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -19,14 +27,26 @@ var (
|
|||
AnswerVoteDown,
|
||||
CommentVoteUp,
|
||||
CommentVoteDown,
|
||||
AnswerAccepted,
|
||||
AnswerAccept,
|
||||
QuestionVotedUp,
|
||||
QuestionVotedDown,
|
||||
AnswerVotedUp,
|
||||
AnswerVotedDown,
|
||||
}
|
||||
activityTypeFlagMapping = map[string]string{
|
||||
QuestionVoteUp: "upvote",
|
||||
QuestionVoteDown: "downvote",
|
||||
AnswerVoteUp: "upvote",
|
||||
AnswerVoteDown: "downvote",
|
||||
CommentVoteUp: "upvote",
|
||||
CommentVoteDown: "downvote",
|
||||
QuestionVoteUp: "upvote",
|
||||
QuestionVoteDown: "downvote",
|
||||
AnswerVoteUp: "upvote",
|
||||
AnswerVoteDown: "downvote",
|
||||
CommentVoteUp: "upvote",
|
||||
CommentVoteDown: "downvote",
|
||||
AnswerAccepted: "accepted",
|
||||
AnswerAccept: "accept",
|
||||
QuestionVotedUp: "upvoted",
|
||||
QuestionVotedDown: "downvoted",
|
||||
AnswerVotedUp: "upvoted",
|
||||
AnswerVotedDown: "downvoted",
|
||||
}
|
||||
)
|
||||
|
||||
|
|
|
@ -25,6 +25,7 @@ import (
|
|||
"github.com/answerdev/answer/pkg/encryption"
|
||||
"github.com/answerdev/answer/pkg/uid"
|
||||
"github.com/segmentfault/pacman/errors"
|
||||
"github.com/segmentfault/pacman/i18n"
|
||||
"github.com/segmentfault/pacman/log"
|
||||
)
|
||||
|
||||
|
@ -126,10 +127,12 @@ func (as *AnswerService) RemoveAnswer(ctx context.Context, req *schema.RemoveAns
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = as.answerActivityService.DeleteAnswer(ctx, answerInfo.ID, answerInfo.CreatedAt, answerInfo.VoteCount)
|
||||
if err != nil {
|
||||
log.Errorf("delete answer activity change failed: %s", err.Error())
|
||||
}
|
||||
// #2372 In order to simplify the process and complexity, as well as to consider if it is in-house,
|
||||
// facing the problem of recovery.
|
||||
//err = as.answerActivityService.DeleteAnswer(ctx, answerInfo.ID, answerInfo.CreatedAt, answerInfo.VoteCount)
|
||||
//if err != nil {
|
||||
// log.Errorf("delete answer activity change failed: %s", err.Error())
|
||||
//}
|
||||
activity_queue.AddActivity(&schema.ActivityMsg{
|
||||
UserID: req.UserID,
|
||||
ObjectID: answerInfo.ID,
|
||||
|
@ -457,17 +460,18 @@ func (as *AnswerService) AdminSetAnswerStatus(ctx context.Context, req *schema.A
|
|||
}
|
||||
|
||||
if setStatus == entity.AnswerStatusDeleted {
|
||||
err = as.answerActivityService.DeleteAnswer(ctx, answerInfo.ID, answerInfo.CreatedAt, answerInfo.VoteCount)
|
||||
if err != nil {
|
||||
log.Errorf("admin delete question then rank rollback error %s", err.Error())
|
||||
} else {
|
||||
activity_queue.AddActivity(&schema.ActivityMsg{
|
||||
UserID: req.UserID,
|
||||
ObjectID: answerInfo.ID,
|
||||
OriginalObjectID: answerInfo.ID,
|
||||
ActivityTypeKey: constant.ActAnswerDeleted,
|
||||
})
|
||||
}
|
||||
// #2372 In order to simplify the process and complexity, as well as to consider if it is in-house,
|
||||
// facing the problem of recovery.
|
||||
//err = as.answerActivityService.DeleteAnswer(ctx, answerInfo.ID, answerInfo.CreatedAt, answerInfo.VoteCount)
|
||||
//if err != nil {
|
||||
// log.Errorf("admin delete question then rank rollback error %s", err.Error())
|
||||
//}
|
||||
activity_queue.AddActivity(&schema.ActivityMsg{
|
||||
UserID: req.UserID,
|
||||
ObjectID: answerInfo.ID,
|
||||
OriginalObjectID: answerInfo.ID,
|
||||
ActivityTypeKey: constant.ActAnswerDeleted,
|
||||
})
|
||||
}
|
||||
|
||||
msg := &schema.NotificationMsg{}
|
||||
|
@ -616,6 +620,10 @@ func (as *AnswerService) notificationAnswerTheQuestion(ctx context.Context,
|
|||
UserID: userInfo.ID,
|
||||
}
|
||||
|
||||
// If receiver has set language, use it to send email.
|
||||
if len(userInfo.Language) > 0 {
|
||||
ctx = context.WithValue(ctx, constant.AcceptLanguageFlag, i18n.Language(userInfo.Language))
|
||||
}
|
||||
title, body, err := as.emailService.NewAnswerTemplate(ctx, rawData)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
|
|
|
@ -22,6 +22,7 @@ import (
|
|||
"github.com/answerdev/answer/pkg/uid"
|
||||
"github.com/jinzhu/copier"
|
||||
"github.com/segmentfault/pacman/errors"
|
||||
"github.com/segmentfault/pacman/i18n"
|
||||
"github.com/segmentfault/pacman/log"
|
||||
)
|
||||
|
||||
|
@ -452,6 +453,9 @@ func (cs *CommentService) GetCommentPersonalWithPage(ctx context.Context, req *s
|
|||
commentResp.UrlTitle = htmltext.UrlTitle(objInfo.Title)
|
||||
commentResp.QuestionID = objInfo.QuestionID
|
||||
commentResp.AnswerID = objInfo.AnswerID
|
||||
if objInfo.QuestionStatus == entity.QuestionStatusDeleted {
|
||||
commentResp.Title = "Deleted question"
|
||||
}
|
||||
}
|
||||
}
|
||||
resp = append(resp, commentResp)
|
||||
|
@ -504,6 +508,10 @@ func (cs *CommentService) notificationQuestionComment(ctx context.Context, quest
|
|||
UserID: receiverUserInfo.ID,
|
||||
}
|
||||
|
||||
// If receiver has set language, use it to send email.
|
||||
if len(receiverUserInfo.Language) > 0 {
|
||||
ctx = context.WithValue(ctx, constant.AcceptLanguageFlag, i18n.Language(receiverUserInfo.Language))
|
||||
}
|
||||
title, body, err := cs.emailService.NewCommentTemplate(ctx, rawData)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
|
@ -560,6 +568,10 @@ func (cs *CommentService) notificationAnswerComment(ctx context.Context,
|
|||
UserID: receiverUserInfo.ID,
|
||||
}
|
||||
|
||||
// If receiver has set language, use it to send email.
|
||||
if len(receiverUserInfo.Language) > 0 {
|
||||
ctx = context.WithValue(ctx, constant.AcceptLanguageFlag, i18n.Language(receiverUserInfo.Language))
|
||||
}
|
||||
title, body, err := cs.emailService.NewCommentTemplate(ctx, rawData)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
|
|
|
@ -1,14 +1,17 @@
|
|||
package export
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"mime"
|
||||
"os"
|
||||
"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/entity"
|
||||
"github.com/answerdev/answer/internal/schema"
|
||||
"github.com/answerdev/answer/internal/service/config"
|
||||
|
@ -128,6 +131,9 @@ func (es *EmailService) Send(ctx context.Context, toEmailAddr, subject, body str
|
|||
if ec.IsSSL() {
|
||||
d.SSL = true
|
||||
}
|
||||
if len(os.Getenv("SKIP_SMTP_TLS_VERIFY")) > 0 {
|
||||
d.TLSConfig = &tls.Config{ServerName: d.Host, InsecureSkipVerify: true}
|
||||
}
|
||||
if err := d.DialAndSend(m); err != nil {
|
||||
log.Errorf("send email to %s failed: %s", toEmailAddr, err)
|
||||
} else {
|
||||
|
@ -162,11 +168,6 @@ func (es *EmailService) GetSiteGeneral(ctx context.Context) (resp schema.SiteGen
|
|||
}
|
||||
|
||||
func (es *EmailService) RegisterTemplate(ctx context.Context, registerUrl string) (title, body string, err error) {
|
||||
emailConfig, err := es.GetEmailConfig()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
siteInfo, err := es.GetSiteGeneral(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
|
@ -176,123 +177,59 @@ func (es *EmailService) RegisterTemplate(ctx context.Context, registerUrl string
|
|||
RegisterUrl: registerUrl,
|
||||
}
|
||||
|
||||
title, err = es.parseTemplateData(emailConfig.RegisterTitle, templateData)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("email template parse error: %s", err)
|
||||
}
|
||||
|
||||
body, err = es.parseTemplateData(emailConfig.RegisterBody, templateData)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("email template parse error: %s", err)
|
||||
}
|
||||
lang := handler.GetLangByCtx(ctx)
|
||||
title = translator.TrWithData(lang, constant.EmailTplKeyRegisterTitle, templateData)
|
||||
body = translator.TrWithData(lang, constant.EmailTplKeyRegisterBody, templateData)
|
||||
return title, body, nil
|
||||
}
|
||||
|
||||
func (es *EmailService) PassResetTemplate(ctx context.Context, passResetUrl string) (title, body string, err error) {
|
||||
ec, err := es.GetEmailConfig()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
siteinfo, err := es.GetSiteGeneral(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
templateData := PassResetTemplateData{SiteName: siteinfo.Name, PassResetUrl: passResetUrl}
|
||||
tmpl, err := template.New("pass_reset_title").Parse(ec.PassResetTitle)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
titleBuf := &bytes.Buffer{}
|
||||
bodyBuf := &bytes.Buffer{}
|
||||
err = tmpl.Execute(titleBuf, templateData)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
tmpl, err = template.New("pass_reset_body").Parse(ec.PassResetBody)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
err = tmpl.Execute(bodyBuf, templateData)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
return titleBuf.String(), bodyBuf.String(), nil
|
||||
}
|
||||
|
||||
func (es *EmailService) ChangeEmailTemplate(ctx context.Context, changeEmailUrl string) (title, body string, err error) {
|
||||
ec, err := es.GetEmailConfig()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
siteinfo, err := es.GetSiteGeneral(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
templateData := ChangeEmailTemplateData{
|
||||
SiteName: siteinfo.Name,
|
||||
ChangeEmailUrl: changeEmailUrl,
|
||||
}
|
||||
tmpl, err := template.New("email_change_title").Parse(ec.ChangeTitle)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
titleBuf := &bytes.Buffer{}
|
||||
bodyBuf := &bytes.Buffer{}
|
||||
err = tmpl.Execute(titleBuf, templateData)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
tmpl, err = template.New("email_change_body").Parse(ec.ChangeBody)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
err = tmpl.Execute(bodyBuf, templateData)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
return titleBuf.String(), bodyBuf.String(), nil
|
||||
}
|
||||
|
||||
// TestTemplate send test email template parse
|
||||
func (es *EmailService) TestTemplate(ctx context.Context) (title, body string, err error) {
|
||||
emailConfig, err := es.GetEmailConfig()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
siteInfo, err := es.GetSiteGeneral(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
templateData := TestTemplateData{
|
||||
SiteName: siteInfo.Name,
|
||||
|
||||
templateData := PassResetTemplateData{SiteName: siteInfo.Name, PassResetUrl: passResetUrl}
|
||||
|
||||
lang := handler.GetLangByCtx(ctx)
|
||||
title = translator.TrWithData(lang, constant.EmailTplKeyPassResetTitle, templateData)
|
||||
body = translator.TrWithData(lang, constant.EmailTplKeyPassResetBody, templateData)
|
||||
return title, body, nil
|
||||
}
|
||||
|
||||
func (es *EmailService) ChangeEmailTemplate(ctx context.Context, changeEmailUrl string) (title, body string, err error) {
|
||||
siteInfo, err := es.GetSiteGeneral(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
templateData := ChangeEmailTemplateData{
|
||||
SiteName: siteInfo.Name,
|
||||
ChangeEmailUrl: changeEmailUrl,
|
||||
}
|
||||
|
||||
title, err = es.parseTemplateData(emailConfig.TestTitle, templateData)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("email template parse error: %s", err)
|
||||
}
|
||||
lang := handler.GetLangByCtx(ctx)
|
||||
title = translator.TrWithData(lang, constant.EmailTplKeyChangeEmailTitle, templateData)
|
||||
body = translator.TrWithData(lang, constant.EmailTplKeyChangeEmailBody, templateData)
|
||||
return title, body, nil
|
||||
}
|
||||
|
||||
body, err = es.parseTemplateData(emailConfig.TestBody, templateData)
|
||||
// TestTemplate send test email template parse
|
||||
func (es *EmailService) TestTemplate(ctx context.Context) (title, body string, err error) {
|
||||
siteInfo, err := es.GetSiteGeneral(ctx)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("email template parse error: %s", err)
|
||||
return
|
||||
}
|
||||
templateData := TestTemplateData{SiteName: siteInfo.Name}
|
||||
|
||||
lang := handler.GetLangByCtx(ctx)
|
||||
title = translator.TrWithData(lang, constant.EmailTplKeyTestTitle, templateData)
|
||||
body = translator.TrWithData(lang, constant.EmailTplKeyTestBody, 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
|
||||
}
|
||||
|
||||
siteInfo, err := es.GetSiteGeneral(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
|
@ -305,28 +242,37 @@ func (es *EmailService) NewAnswerTemplate(ctx context.Context, raw *schema.NewAn
|
|||
AnswerSummary: raw.AnswerSummary,
|
||||
UnsubscribeUrl: fmt.Sprintf("%s/users/unsubscribe?code=%s", siteInfo.SiteUrl, raw.UnsubscribeCode),
|
||||
}
|
||||
templateData.SiteName = siteInfo.Name
|
||||
|
||||
title, err = es.parseTemplateData(emailConfig.NewAnswerTitle, templateData)
|
||||
lang := handler.GetLangByCtx(ctx)
|
||||
title = translator.TrWithData(lang, constant.EmailTplKeyNewAnswerTitle, templateData)
|
||||
body = translator.TrWithData(lang, constant.EmailTplKeyNewAnswerBody, templateData)
|
||||
return title, body, nil
|
||||
}
|
||||
|
||||
// NewInviteAnswerTemplate new invite answer template
|
||||
func (es *EmailService) NewInviteAnswerTemplate(ctx context.Context, raw *schema.NewInviteAnswerTemplateRawData) (
|
||||
title, body string, err error) {
|
||||
siteInfo, err := es.GetSiteGeneral(ctx)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("email template parse error: %s", err)
|
||||
return
|
||||
}
|
||||
templateData := &schema.NewInviteAnswerTemplateData{
|
||||
SiteName: siteInfo.Name,
|
||||
DisplayName: raw.InviterDisplayName,
|
||||
QuestionTitle: raw.QuestionTitle,
|
||||
InviteUrl: fmt.Sprintf("%s/questions/%s", siteInfo.SiteUrl, raw.QuestionID),
|
||||
UnsubscribeUrl: fmt.Sprintf("%s/users/unsubscribe?code=%s", siteInfo.SiteUrl, raw.UnsubscribeCode),
|
||||
}
|
||||
|
||||
body, err = es.parseTemplateData(emailConfig.NewAnswerBody, templateData)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("email template parse error: %s", err)
|
||||
}
|
||||
lang := handler.GetLangByCtx(ctx)
|
||||
title = translator.TrWithData(lang, constant.EmailTplKeyInvitedAnswerTitle, templateData)
|
||||
body = translator.TrWithData(lang, constant.EmailTplKeyInvitedAnswerBody, templateData)
|
||||
return title, body, nil
|
||||
}
|
||||
|
||||
// NewCommentTemplate new comment template
|
||||
func (es *EmailService) NewCommentTemplate(ctx context.Context, raw *schema.NewCommentTemplateRawData) (
|
||||
title, body string, err error) {
|
||||
emailConfig, err := es.GetEmailConfig()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
siteInfo, err := es.GetSiteGeneral(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
|
@ -345,33 +291,13 @@ func (es *EmailService) NewCommentTemplate(ctx context.Context, raw *schema.NewC
|
|||
templateData.CommentUrl = fmt.Sprintf("%s/questions/%s?commentId=%s", siteInfo.SiteUrl,
|
||||
raw.QuestionID, raw.CommentID)
|
||||
}
|
||||
templateData.SiteName = siteInfo.Name
|
||||
|
||||
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)
|
||||
}
|
||||
lang := handler.GetLangByCtx(ctx)
|
||||
title = translator.TrWithData(lang, constant.EmailTplKeyNewCommentTitle, templateData)
|
||||
body = translator.TrWithData(lang, constant.EmailTplKeyNewCommentBody, templateData)
|
||||
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) {
|
||||
emailConf, err := es.configRepo.GetString("email.config")
|
||||
if err != nil {
|
||||
|
|
|
@ -123,7 +123,15 @@ func (ns *NotificationService) GetNotificationPage(ctx context.Context, searchCo
|
|||
if !ok {
|
||||
return pager.NewPageModel(0, resp), nil
|
||||
}
|
||||
searchInboxType := schema.NotificationInboxTypeAll
|
||||
if searchType == schema.NotificationTypeInbox {
|
||||
searchInboxType, ok = schema.NotificationInboxType[searchCond.InboxTypeStr]
|
||||
if !ok {
|
||||
return pager.NewPageModel(0, resp), nil
|
||||
}
|
||||
}
|
||||
searchCond.Type = searchType
|
||||
searchCond.InboxType = searchInboxType
|
||||
notifications, total, err := ns.notificationRepo.GetNotificationPage(ctx, searchCond)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
|
@ -135,6 +135,7 @@ func (os *ObjService) GetInfo(ctx context.Context, objectID string) (objInfo *sc
|
|||
ObjectID: questionInfo.ID,
|
||||
ObjectCreatorUserID: questionInfo.UserID,
|
||||
QuestionID: questionInfo.ID,
|
||||
QuestionStatus: questionInfo.Status,
|
||||
ObjectType: objectType,
|
||||
Title: questionInfo.Title,
|
||||
Content: questionInfo.ParsedText, // todo trim
|
||||
|
@ -158,6 +159,7 @@ func (os *ObjService) GetInfo(ctx context.Context, objectID string) (objInfo *sc
|
|||
ObjectID: answerInfo.ID,
|
||||
ObjectCreatorUserID: answerInfo.UserID,
|
||||
QuestionID: answerInfo.QuestionID,
|
||||
QuestionStatus: questionInfo.Status,
|
||||
AnswerID: answerInfo.ID,
|
||||
ObjectType: objectType,
|
||||
Title: questionInfo.Title, // this should be question title
|
||||
|
@ -185,6 +187,7 @@ func (os *ObjService) GetInfo(ctx context.Context, objectID string) (objInfo *sc
|
|||
}
|
||||
if exist {
|
||||
objInfo.QuestionID = questionInfo.ID
|
||||
objInfo.QuestionStatus = questionInfo.Status
|
||||
objInfo.Title = questionInfo.Title
|
||||
}
|
||||
answerInfo, exist, err := os.answerRepo.GetAnswer(ctx, commentInfo.ObjectID)
|
||||
|
|
|
@ -1,54 +1,56 @@
|
|||
package permission
|
||||
|
||||
const (
|
||||
AdminAccess = "admin.access"
|
||||
QuestionAdd = "question.add"
|
||||
QuestionEdit = "question.edit"
|
||||
QuestionEditWithoutReview = "question.edit_without_review"
|
||||
QuestionDelete = "question.delete"
|
||||
QuestionClose = "question.close"
|
||||
QuestionReopen = "question.reopen"
|
||||
QuestionVoteUp = "question.vote_up"
|
||||
QuestionVoteDown = "question.vote_down"
|
||||
QuestionPin = "question.pin"
|
||||
QuestionUnPin = "question.unpin"
|
||||
QuestionHide = "question.hide"
|
||||
QuestionShow = "question.show"
|
||||
AnswerAdd = "answer.add"
|
||||
AnswerEdit = "answer.edit"
|
||||
AnswerEditWithoutReview = "answer.edit_without_review"
|
||||
AnswerDelete = "answer.delete"
|
||||
AnswerAccept = "answer.accept"
|
||||
AnswerVoteUp = "answer.vote_up"
|
||||
AnswerVoteDown = "answer.vote_down"
|
||||
CommentAdd = "comment.add"
|
||||
CommentEdit = "comment.edit"
|
||||
CommentDelete = "comment.delete"
|
||||
CommentVoteUp = "comment.vote_up"
|
||||
CommentVoteDown = "comment.vote_down"
|
||||
ReportAdd = "report.add"
|
||||
TagAdd = "tag.add"
|
||||
TagEdit = "tag.edit"
|
||||
TagEditSlugName = "tag.edit_slug_name"
|
||||
TagEditWithoutReview = "tag.edit_without_review"
|
||||
TagDelete = "tag.delete"
|
||||
TagSynonym = "tag.synonym"
|
||||
LinkUrlLimit = "link.url_limit"
|
||||
VoteDetail = "vote.detail"
|
||||
AnswerAudit = "answer.audit"
|
||||
QuestionAudit = "question.audit"
|
||||
TagAudit = "tag.audit"
|
||||
TagUseReservedTag = "tag.use_reserved_tag"
|
||||
AdminAccess = "admin.access"
|
||||
QuestionAdd = "question.add"
|
||||
QuestionEdit = "question.edit"
|
||||
QuestionEditWithoutReview = "question.edit_without_review"
|
||||
QuestionDelete = "question.delete"
|
||||
QuestionClose = "question.close"
|
||||
QuestionReopen = "question.reopen"
|
||||
QuestionVoteUp = "question.vote_up"
|
||||
QuestionVoteDown = "question.vote_down"
|
||||
QuestionPin = "question.pin"
|
||||
QuestionUnPin = "question.unpin"
|
||||
QuestionHide = "question.hide"
|
||||
QuestionShow = "question.show"
|
||||
AnswerAdd = "answer.add"
|
||||
AnswerEdit = "answer.edit"
|
||||
AnswerEditWithoutReview = "answer.edit_without_review"
|
||||
AnswerDelete = "answer.delete"
|
||||
AnswerAccept = "answer.accept"
|
||||
AnswerVoteUp = "answer.vote_up"
|
||||
AnswerVoteDown = "answer.vote_down"
|
||||
AnswerInviteSomeoneToAnswer = "answer.invite_someone_to_answer"
|
||||
CommentAdd = "comment.add"
|
||||
CommentEdit = "comment.edit"
|
||||
CommentDelete = "comment.delete"
|
||||
CommentVoteUp = "comment.vote_up"
|
||||
CommentVoteDown = "comment.vote_down"
|
||||
ReportAdd = "report.add"
|
||||
TagAdd = "tag.add"
|
||||
TagEdit = "tag.edit"
|
||||
TagEditSlugName = "tag.edit_slug_name"
|
||||
TagEditWithoutReview = "tag.edit_without_review"
|
||||
TagDelete = "tag.delete"
|
||||
TagSynonym = "tag.synonym"
|
||||
LinkUrlLimit = "link.url_limit"
|
||||
VoteDetail = "vote.detail"
|
||||
AnswerAudit = "answer.audit"
|
||||
QuestionAudit = "question.audit"
|
||||
TagAudit = "tag.audit"
|
||||
TagUseReservedTag = "tag.use_reserved_tag"
|
||||
)
|
||||
|
||||
const (
|
||||
reportActionName = "action.report"
|
||||
editActionName = "action.edit"
|
||||
deleteActionName = "action.delete"
|
||||
closeActionName = "action.close"
|
||||
reopenActionName = "action.reopen"
|
||||
pinActionName = "action.pin"
|
||||
unpinActionName = "action.unpin"
|
||||
hideActionName = "action.hide"
|
||||
showActionName = "action.show"
|
||||
reportActionName = "action.report"
|
||||
editActionName = "action.edit"
|
||||
deleteActionName = "action.delete"
|
||||
closeActionName = "action.close"
|
||||
reopenActionName = "action.reopen"
|
||||
pinActionName = "action.pin"
|
||||
unpinActionName = "action.unpin"
|
||||
hideActionName = "action.hide"
|
||||
showActionName = "action.show"
|
||||
inviteSomeoneToAnswerActionName = "action.invite_someone_to_answer"
|
||||
)
|
||||
|
|
|
@ -81,3 +81,19 @@ func GetQuestionPermission(ctx context.Context, userID string, creatorUserID str
|
|||
}
|
||||
return actions
|
||||
}
|
||||
|
||||
// GetQuestionExtendsPermission get question extends permission
|
||||
func GetQuestionExtendsPermission(ctx context.Context, userID string, creatorUserID string,
|
||||
canInviteOtherToAnswer bool) (
|
||||
actions []*schema.PermissionMemberAction) {
|
||||
lang := handler.GetLangByCtx(ctx)
|
||||
actions = make([]*schema.PermissionMemberAction, 0)
|
||||
if canInviteOtherToAnswer || userID == creatorUserID {
|
||||
actions = append(actions, &schema.PermissionMemberAction{
|
||||
Action: "invite_other_to_answer",
|
||||
Name: translator.Tr(lang, inviteSomeoneToAnswerActionName),
|
||||
Type: "confirm",
|
||||
})
|
||||
}
|
||||
return actions
|
||||
}
|
||||
|
|
|
@ -46,6 +46,8 @@ type QuestionRepo interface {
|
|||
FindByID(ctx context.Context, id []string) (questionList []*entity.Question, err error)
|
||||
AdminSearchList(ctx context.Context, search *schema.AdminQuestionSearch) ([]*entity.Question, int64, error)
|
||||
GetQuestionCount(ctx context.Context) (count int64, err error)
|
||||
GetUserQuestionCount(ctx context.Context, userID string) (count int64, err error)
|
||||
GetQuestionCountByIDs(ctx context.Context, ids []string) (count int64, err error)
|
||||
GetQuestionIDsPage(ctx context.Context, page, pageSize int) (questionIDList []*schema.SiteMapQuestionInfo, err error)
|
||||
}
|
||||
|
||||
|
@ -88,6 +90,10 @@ func NewQuestionCommon(questionRepo QuestionRepo,
|
|||
}
|
||||
}
|
||||
|
||||
func (qs *QuestionCommon) GetUserQuestionCount(ctx context.Context, userID string) (count int64, err error) {
|
||||
return qs.questionRepo.GetUserQuestionCount(ctx, userID)
|
||||
}
|
||||
|
||||
func (qs *QuestionCommon) UpdatePv(ctx context.Context, questionID string) error {
|
||||
return qs.questionRepo.UpdatePvCount(ctx, questionID)
|
||||
}
|
||||
|
@ -144,6 +150,34 @@ func (qs *QuestionCommon) FindInfoByID(ctx context.Context, questionIDs []string
|
|||
return list, nil
|
||||
}
|
||||
|
||||
func (qs *QuestionCommon) InviteUserInfo(ctx context.Context, questionID string) (inviteList []*schema.UserBasicInfo, err error) {
|
||||
InviteUserInfo := make([]*schema.UserBasicInfo, 0)
|
||||
dbinfo, has, err := qs.questionRepo.GetQuestion(ctx, questionID)
|
||||
if err != nil {
|
||||
return InviteUserInfo, err
|
||||
}
|
||||
if !has {
|
||||
return InviteUserInfo, errors.NotFound(reason.QuestionNotFound)
|
||||
}
|
||||
//InviteUser
|
||||
if dbinfo.InviteUserID != "" {
|
||||
InviteUserIDs := make([]string, 0)
|
||||
err := json.Unmarshal([]byte(dbinfo.InviteUserID), &InviteUserIDs)
|
||||
if err == nil {
|
||||
inviteUserInfoMap, err := qs.userCommon.BatchUserBasicInfoByID(ctx, InviteUserIDs)
|
||||
if err == nil {
|
||||
for _, userid := range InviteUserIDs {
|
||||
_, ok := inviteUserInfoMap[userid]
|
||||
if ok {
|
||||
InviteUserInfo = append(InviteUserInfo, inviteUserInfoMap[userid])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return InviteUserInfo, nil
|
||||
}
|
||||
|
||||
func (qs *QuestionCommon) Info(ctx context.Context, questionID string, loginUserID string) (showinfo *schema.QuestionInfo, err error) {
|
||||
dbinfo, has, err := qs.questionRepo.GetQuestion(ctx, questionID)
|
||||
if err != nil {
|
||||
|
@ -180,9 +214,7 @@ func (qs *QuestionCommon) Info(ctx context.Context, questionID string, loginUser
|
|||
operation.Level = schema.OperationLevelInfo
|
||||
showinfo.Operation = operation
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -431,14 +463,16 @@ func (qs *QuestionCommon) RemoveQuestion(ctx context.Context, req *schema.Remove
|
|||
return err
|
||||
}
|
||||
|
||||
// user add question count
|
||||
err = qs.userCommon.UpdateQuestionCount(ctx, questionInfo.UserID, -1)
|
||||
userQuestionCount, err := qs.GetUserQuestionCount(ctx, questionInfo.UserID)
|
||||
if err != nil {
|
||||
log.Error("user UpdateQuestionCount error", err.Error())
|
||||
log.Error("user GetUserQuestionCount error", err.Error())
|
||||
} else {
|
||||
err = qs.userCommon.UpdateQuestionCount(ctx, questionInfo.UserID, userQuestionCount)
|
||||
if err != nil {
|
||||
log.Error("user IncreaseQuestionCount error", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// todo rank remove
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
@ -19,6 +19,7 @@ import (
|
|||
"github.com/answerdev/answer/internal/service/activity"
|
||||
"github.com/answerdev/answer/internal/service/activity_queue"
|
||||
collectioncommon "github.com/answerdev/answer/internal/service/collection_common"
|
||||
"github.com/answerdev/answer/internal/service/export"
|
||||
"github.com/answerdev/answer/internal/service/meta"
|
||||
"github.com/answerdev/answer/internal/service/notice_queue"
|
||||
"github.com/answerdev/answer/internal/service/permission"
|
||||
|
@ -26,6 +27,7 @@ import (
|
|||
"github.com/answerdev/answer/internal/service/revision_common"
|
||||
tagcommon "github.com/answerdev/answer/internal/service/tag_common"
|
||||
usercommon "github.com/answerdev/answer/internal/service/user_common"
|
||||
"github.com/answerdev/answer/pkg/encryption"
|
||||
"github.com/answerdev/answer/pkg/htmltext"
|
||||
"github.com/answerdev/answer/pkg/uid"
|
||||
"github.com/jinzhu/copier"
|
||||
|
@ -43,11 +45,13 @@ type QuestionService struct {
|
|||
tagCommon *tagcommon.TagCommonService
|
||||
questioncommon *questioncommon.QuestionCommon
|
||||
userCommon *usercommon.UserCommon
|
||||
userRepo usercommon.UserRepo
|
||||
revisionService *revision_common.RevisionService
|
||||
metaService *meta.MetaService
|
||||
collectionCommon *collectioncommon.CollectionCommon
|
||||
answerActivityService *activity.AnswerActivityService
|
||||
data *data.Data
|
||||
emailService *export.EmailService
|
||||
}
|
||||
|
||||
func NewQuestionService(
|
||||
|
@ -55,23 +59,26 @@ func NewQuestionService(
|
|||
tagCommon *tagcommon.TagCommonService,
|
||||
questioncommon *questioncommon.QuestionCommon,
|
||||
userCommon *usercommon.UserCommon,
|
||||
userRepo usercommon.UserRepo,
|
||||
revisionService *revision_common.RevisionService,
|
||||
metaService *meta.MetaService,
|
||||
collectionCommon *collectioncommon.CollectionCommon,
|
||||
answerActivityService *activity.AnswerActivityService,
|
||||
data *data.Data,
|
||||
|
||||
emailService *export.EmailService,
|
||||
) *QuestionService {
|
||||
return &QuestionService{
|
||||
questionRepo: questionRepo,
|
||||
tagCommon: tagCommon,
|
||||
questioncommon: questioncommon,
|
||||
userCommon: userCommon,
|
||||
userRepo: userRepo,
|
||||
revisionService: revisionService,
|
||||
metaService: metaService,
|
||||
collectionCommon: collectionCommon,
|
||||
answerActivityService: answerActivityService,
|
||||
data: data,
|
||||
emailService: emailService,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -132,22 +139,6 @@ func (qs *QuestionService) ReopenQuestion(ctx context.Context, req *schema.Reope
|
|||
return nil
|
||||
}
|
||||
|
||||
// CloseMsgList list close question condition
|
||||
func (qs *QuestionService) CloseMsgList(ctx context.Context, lang i18n.Language) (
|
||||
resp []*schema.GetCloseTypeResp, err error,
|
||||
) {
|
||||
resp = make([]*schema.GetCloseTypeResp, 0)
|
||||
err = json.Unmarshal([]byte(constant.QuestionCloseJSON), &resp)
|
||||
if err != nil {
|
||||
return nil, errors.InternalServer(reason.UnknownError).WithError(err).WithStack()
|
||||
}
|
||||
for _, t := range resp {
|
||||
t.Name = translator.Tr(lang, t.Name)
|
||||
t.Description = translator.Tr(lang, t.Description)
|
||||
}
|
||||
return resp, err
|
||||
}
|
||||
|
||||
func (qs *QuestionService) AddQuestionCheckTags(ctx context.Context, Tags []*entity.Tag) ([]string, error) {
|
||||
list := make([]string, 0)
|
||||
for _, tag := range Tags {
|
||||
|
@ -306,9 +297,14 @@ func (qs *QuestionService) AddQuestion(ctx context.Context, req *schema.Question
|
|||
}
|
||||
|
||||
// user add question count
|
||||
err = qs.userCommon.UpdateQuestionCount(ctx, question.UserID, 1)
|
||||
userQuestionCount, err := qs.questioncommon.GetUserQuestionCount(ctx, question.UserID)
|
||||
if err != nil {
|
||||
log.Error("user IncreaseQuestionCount error", err.Error())
|
||||
log.Error("user GetUserQuestionCount error", err.Error())
|
||||
} else {
|
||||
err = qs.userCommon.UpdateQuestionCount(ctx, question.UserID, userQuestionCount)
|
||||
if err != nil {
|
||||
log.Error("user IncreaseQuestionCount error", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
activity_queue.AddActivity(&schema.ActivityMsg{
|
||||
|
@ -344,8 +340,24 @@ func (qs *QuestionService) OperationQuestion(ctx context.Context, req *schema.Op
|
|||
switch req.Operation {
|
||||
case schema.QuestionOperationHide:
|
||||
questionInfo.Show = entity.QuestionHide
|
||||
err = qs.tagCommon.HideTagRelListByObjectID(ctx, req.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = qs.tagCommon.RefreshTagCountByQuestionID(ctx, req.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case schema.QuestionOperationShow:
|
||||
questionInfo.Show = entity.QuestionShow
|
||||
err = qs.tagCommon.ShowTagRelListByObjectID(ctx, req.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = qs.tagCommon.RefreshTagCountByQuestionID(ctx, req.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case schema.QuestionOperationPin:
|
||||
questionInfo.Pin = entity.QuestionPin
|
||||
case schema.QuestionOperationUnPin:
|
||||
|
@ -421,16 +433,41 @@ func (qs *QuestionService) RemoveQuestion(ctx context.Context, req *schema.Remov
|
|||
return err
|
||||
}
|
||||
|
||||
// user add question count
|
||||
err = qs.userCommon.UpdateQuestionCount(ctx, questionInfo.UserID, -1)
|
||||
userQuestionCount, err := qs.questioncommon.GetUserQuestionCount(ctx, questionInfo.UserID)
|
||||
if err != nil {
|
||||
log.Error("user IncreaseQuestionCount error", err.Error())
|
||||
log.Error("user GetUserQuestionCount error", err.Error())
|
||||
} else {
|
||||
err = qs.userCommon.UpdateQuestionCount(ctx, questionInfo.UserID, userQuestionCount)
|
||||
if err != nil {
|
||||
log.Error("user IncreaseQuestionCount error", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
err = qs.answerActivityService.DeleteQuestion(ctx, questionInfo.ID, questionInfo.CreatedAt, questionInfo.VoteCount)
|
||||
if err != nil {
|
||||
log.Errorf("user DeleteQuestion rank rollback error %s", err.Error())
|
||||
//tag count
|
||||
tagIDs := make([]string, 0)
|
||||
Tags, tagerr := qs.tagCommon.GetObjectEntityTag(ctx, req.ID)
|
||||
if tagerr != nil {
|
||||
log.Error("GetObjectEntityTag error", tagerr)
|
||||
return nil
|
||||
}
|
||||
for _, v := range Tags {
|
||||
tagIDs = append(tagIDs, v.ID)
|
||||
}
|
||||
err = qs.tagCommon.RemoveTagRelListByObjectID(ctx, req.ID)
|
||||
if err != nil {
|
||||
log.Error("RemoveTagRelListByObjectID error", err.Error())
|
||||
}
|
||||
err = qs.tagCommon.RefreshTagQuestionCount(ctx, tagIDs)
|
||||
if err != nil {
|
||||
log.Error("efreshTagQuestionCount error", err.Error())
|
||||
}
|
||||
|
||||
// #2372 In order to simplify the process and complexity, as well as to consider if it is in-house,
|
||||
// facing the problem of recovery.
|
||||
// err = qs.answerActivityService.DeleteQuestion(ctx, questionInfo.ID, questionInfo.CreatedAt, questionInfo.VoteCount)
|
||||
// if err != nil {
|
||||
// log.Errorf("user DeleteQuestion rank rollback error %s", err.Error())
|
||||
// }
|
||||
activity_queue.AddActivity(&schema.ActivityMsg{
|
||||
UserID: req.UserID,
|
||||
ObjectID: questionInfo.ID,
|
||||
|
@ -506,6 +543,115 @@ func (qs *QuestionService) UpdateQuestionCheckTags(ctx context.Context, req *sch
|
|||
return nil, nil
|
||||
}
|
||||
|
||||
func (qs *QuestionService) UpdateQuestionInviteUser(ctx context.Context, req *schema.QuestionUpdateInviteUser) (err error) {
|
||||
originQuestion, exist, err := qs.questionRepo.GetQuestion(ctx, req.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !exist {
|
||||
return errors.NotFound(reason.ObjectNotFound)
|
||||
}
|
||||
|
||||
//verify invite user
|
||||
inviteUserInfoList, err := qs.userCommon.BatchGetUserBasicInfoByUserNames(ctx, req.InviteUser)
|
||||
if err != nil {
|
||||
log.Error("BatchGetUserBasicInfoByUserNames error", err.Error())
|
||||
}
|
||||
inviteUserIDs := make([]string, 0)
|
||||
for _, item := range req.InviteUser {
|
||||
_, ok := inviteUserInfoList[item]
|
||||
if ok {
|
||||
inviteUserIDs = append(inviteUserIDs, inviteUserInfoList[item].ID)
|
||||
}
|
||||
}
|
||||
inviteUserStr := ""
|
||||
inviteUserByte, err := json.Marshal(inviteUserIDs)
|
||||
if err != nil {
|
||||
log.Error("json.Marshal error", err.Error())
|
||||
inviteUserStr = "[]"
|
||||
} else {
|
||||
inviteUserStr = string(inviteUserByte)
|
||||
}
|
||||
question := &entity.Question{}
|
||||
question.ID = uid.DeShortID(req.ID)
|
||||
question.InviteUserID = inviteUserStr
|
||||
|
||||
saveerr := qs.questionRepo.UpdateQuestion(ctx, question, []string{"invite_user_id"})
|
||||
if saveerr != nil {
|
||||
return saveerr
|
||||
}
|
||||
go qs.notificationInviteUser(ctx, inviteUserIDs, originQuestion.ID, originQuestion.Title, req.UserID)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (qs *QuestionService) notificationInviteUser(
|
||||
ctx context.Context, invitedUserIDs []string, questionID, questionTitle, questionUserID string) {
|
||||
inviter, exist, err := qs.userCommon.GetUserBasicInfoByID(ctx, questionUserID)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return
|
||||
}
|
||||
if !exist {
|
||||
log.Warnf("user %s not found", questionUserID)
|
||||
return
|
||||
}
|
||||
|
||||
users, err := qs.userRepo.BatchGetByID(ctx, invitedUserIDs)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return
|
||||
}
|
||||
invitee := make(map[string]*entity.User, len(users))
|
||||
for _, user := range users {
|
||||
invitee[user.ID] = user
|
||||
}
|
||||
for _, userID := range invitedUserIDs {
|
||||
msg := &schema.NotificationMsg{
|
||||
ReceiverUserID: userID,
|
||||
TriggerUserID: questionUserID,
|
||||
Type: schema.NotificationTypeInbox,
|
||||
ObjectID: questionID,
|
||||
}
|
||||
msg.ObjectType = constant.QuestionObjectType
|
||||
msg.NotificationAction = constant.NotificationInvitedYouToAnswer
|
||||
notice_queue.AddNotification(msg)
|
||||
|
||||
userInfo, ok := invitee[userID]
|
||||
if !ok {
|
||||
log.Warnf("user %s not found", userID)
|
||||
return
|
||||
}
|
||||
if userInfo.NoticeStatus == schema.NoticeStatusOff || len(userInfo.EMail) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
rawData := &schema.NewInviteAnswerTemplateRawData{
|
||||
InviterDisplayName: inviter.DisplayName,
|
||||
QuestionTitle: questionTitle,
|
||||
QuestionID: questionID,
|
||||
UnsubscribeCode: encryption.MD5(userInfo.Pass),
|
||||
}
|
||||
codeContent := &schema.EmailCodeContent{
|
||||
SourceType: schema.UnsubscribeSourceType,
|
||||
Email: userInfo.EMail,
|
||||
UserID: userInfo.ID,
|
||||
}
|
||||
|
||||
// If receiver has set language, use it to send email.
|
||||
if len(userInfo.Language) > 0 {
|
||||
ctx = context.WithValue(ctx, constant.AcceptLanguageFlag, i18n.Language(userInfo.Language))
|
||||
}
|
||||
title, body, err := qs.emailService.NewInviteAnswerTemplate(ctx, rawData)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
go qs.emailService.SendAndSaveCodeWithTime(
|
||||
ctx, userInfo.EMail, title, body, rawData.UnsubscribeCode, codeContent.ToJSONString(), 7*24*time.Hour)
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateQuestion update question
|
||||
func (qs *QuestionService) UpdateQuestion(ctx context.Context, req *schema.QuestionUpdate) (questionInfo any, err error) {
|
||||
var canUpdate bool
|
||||
|
@ -713,6 +859,7 @@ func (qs *QuestionService) GetQuestion(ctx context.Context, questionID, userID s
|
|||
question.Description = htmltext.FetchExcerpt(question.HTML, "...", 240)
|
||||
question.MemberActions = permission.GetQuestionPermission(ctx, userID, question.UserID,
|
||||
per.CanEdit, per.CanDelete, per.CanClose, per.CanReopen, per.CanPin, per.CanHide, per.CanUnPin, per.CanShow)
|
||||
question.ExtendsActions = permission.GetQuestionExtendsPermission(ctx, userID, question.UserID, per.CanInviteOtherToAnswer)
|
||||
return question, nil
|
||||
}
|
||||
|
||||
|
@ -727,6 +874,10 @@ func (qs *QuestionService) GetQuestionAndAddPV(ctx context.Context, questionID,
|
|||
return qs.GetQuestion(ctx, questionID, loginUserID, per)
|
||||
}
|
||||
|
||||
func (qs *QuestionService) InviteUserInfo(ctx context.Context, questionID string) (inviteList []*schema.UserBasicInfo, err error) {
|
||||
return qs.questioncommon.InviteUserInfo(ctx, questionID)
|
||||
}
|
||||
|
||||
func (qs *QuestionService) ChangeTag(ctx context.Context, objectTagData *schema.TagChange) error {
|
||||
return qs.tagCommon.ObjectChangeTag(ctx, objectTagData)
|
||||
}
|
||||
|
@ -809,14 +960,18 @@ func (qs *QuestionService) PersonalAnswerPage(ctx context.Context, req *schema.P
|
|||
_, ok := questionMaps[item.QuestionID]
|
||||
if ok {
|
||||
item.QuestionInfo = questionMaps[item.QuestionID]
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
info := &schema.UserAnswerInfo{}
|
||||
_ = copier.Copy(info, item)
|
||||
info.AnswerID = item.ID
|
||||
info.QuestionID = item.QuestionID
|
||||
if item.QuestionInfo.Status != entity.QuestionStatusDeleted {
|
||||
userAnswerlist = append(userAnswerlist, info)
|
||||
if item.QuestionInfo.Status == entity.QuestionStatusDeleted {
|
||||
info.QuestionInfo.Title = "Deleted question"
|
||||
|
||||
}
|
||||
userAnswerlist = append(userAnswerlist, info)
|
||||
}
|
||||
|
||||
return pager.NewPageModel(total, userAnswerlist), nil
|
||||
|
@ -850,6 +1005,9 @@ func (qs *QuestionService) PersonalCollectionPage(ctx context.Context, req *sche
|
|||
questionMaps[uid.EnShortID(id)].UpdateUserInfo = nil
|
||||
questionMaps[uid.EnShortID(id)].Content = ""
|
||||
questionMaps[uid.EnShortID(id)].HTML = ""
|
||||
if questionMaps[uid.EnShortID(id)].Status == entity.QuestionStatusDeleted {
|
||||
questionMaps[uid.EnShortID(id)].Title = "Deleted question"
|
||||
}
|
||||
list = append(list, questionMaps[uid.EnShortID(id)])
|
||||
}
|
||||
}
|
||||
|
@ -1031,10 +1189,12 @@ func (qs *QuestionService) AdminSetQuestionStatus(ctx context.Context, questionI
|
|||
}
|
||||
|
||||
if setStatus == entity.QuestionStatusDeleted {
|
||||
err = qs.answerActivityService.DeleteQuestion(ctx, questionInfo.ID, questionInfo.CreatedAt, questionInfo.VoteCount)
|
||||
if err != nil {
|
||||
log.Errorf("admin delete question then rank rollback error %s", err.Error())
|
||||
}
|
||||
// #2372 In order to simplify the process and complexity, as well as to consider if it is in-house,
|
||||
// facing the problem of recovery.
|
||||
//err = qs.answerActivityService.DeleteQuestion(ctx, questionInfo.ID, questionInfo.CreatedAt, questionInfo.VoteCount)
|
||||
//if err != nil {
|
||||
// log.Errorf("admin delete question then rank rollback error %s", err.Error())
|
||||
//}
|
||||
activity_queue.AddActivity(&schema.ActivityMsg{
|
||||
UserID: questionInfo.UserID,
|
||||
ObjectID: questionInfo.ID,
|
||||
|
|
|
@ -269,6 +269,9 @@ func (rs *RankService) GetRankPersonalWithPage(ctx context.Context, req *schema.
|
|||
commentResp.Title = objInfo.Title
|
||||
commentResp.UrlTitle = htmltext.UrlTitle(objInfo.Title)
|
||||
commentResp.Content = objInfo.Content
|
||||
if objInfo.QuestionStatus == entity.QuestionStatusDeleted {
|
||||
commentResp.Title = "Deleted question"
|
||||
}
|
||||
commentResp.QuestionID = objInfo.QuestionID
|
||||
commentResp.AnswerID = objInfo.AnswerID
|
||||
}
|
||||
|
|
|
@ -17,6 +17,6 @@ func NewReasonService(reasonRepo reason_common.ReasonRepo) *ReasonService {
|
|||
}
|
||||
}
|
||||
|
||||
func (rs ReasonService) GetReasons(ctx context.Context, req schema.ReasonReq) (resp []schema.ReasonItem, err error) {
|
||||
func (rs ReasonService) GetReasons(ctx context.Context, req schema.ReasonReq) (resp []*schema.ReasonItem, err error) {
|
||||
return rs.reasonRepo.ListReasons(ctx, req.ObjectType, req.Action)
|
||||
}
|
||||
|
|
|
@ -7,5 +7,5 @@ import (
|
|||
)
|
||||
|
||||
type ReasonRepo interface {
|
||||
ListReasons(ctx context.Context, objectType, action string) (resp []schema.ReasonItem, err error)
|
||||
ListReasons(ctx context.Context, objectType, action string) (resp []*schema.ReasonItem, err error)
|
||||
}
|
||||
|
|
|
@ -1,18 +1,11 @@
|
|||
package report
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/answerdev/answer/internal/base/constant"
|
||||
"github.com/answerdev/answer/internal/base/reason"
|
||||
"github.com/answerdev/answer/internal/base/translator"
|
||||
"github.com/answerdev/answer/internal/entity"
|
||||
"github.com/answerdev/answer/internal/schema"
|
||||
"github.com/answerdev/answer/internal/service/object_info"
|
||||
"github.com/answerdev/answer/internal/service/report_common"
|
||||
"github.com/answerdev/answer/pkg/obj"
|
||||
"github.com/segmentfault/pacman/errors"
|
||||
"github.com/segmentfault/pacman/i18n"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
|
@ -56,26 +49,3 @@ func (rs *ReportService) AddReport(ctx context.Context, req *schema.AddReportReq
|
|||
}
|
||||
return rs.reportRepo.AddReport(ctx, report)
|
||||
}
|
||||
|
||||
// GetReportTypeList get report list all
|
||||
func (rs *ReportService) GetReportTypeList(ctx context.Context, lang i18n.Language, req *schema.GetReportListReq) (
|
||||
resp []*schema.GetReportTypeResp, err error,
|
||||
) {
|
||||
resp = make([]*schema.GetReportTypeResp, 0)
|
||||
switch req.Source {
|
||||
case constant.QuestionObjectType:
|
||||
err = json.Unmarshal([]byte(constant.QuestionReportJSON), &resp)
|
||||
case constant.AnswerObjectType:
|
||||
err = json.Unmarshal([]byte(constant.AnswerReportJSON), &resp)
|
||||
case constant.CommentObjectType:
|
||||
err = json.Unmarshal([]byte(constant.CommentReportJSON), &resp)
|
||||
}
|
||||
if err != nil {
|
||||
err = errors.BadRequest(reason.UnknownError)
|
||||
}
|
||||
for _, t := range resp {
|
||||
t.Name = translator.Tr(lang, t.Name)
|
||||
t.Description = translator.Tr(lang, t.Description)
|
||||
}
|
||||
return resp, err
|
||||
}
|
||||
|
|
|
@ -3,6 +3,8 @@ package report_admin
|
|||
import (
|
||||
"context"
|
||||
|
||||
"github.com/answerdev/answer/internal/base/handler"
|
||||
configrepo "github.com/answerdev/answer/internal/repo/config"
|
||||
"github.com/answerdev/answer/internal/service/config"
|
||||
"github.com/answerdev/answer/internal/service/object_info"
|
||||
"github.com/answerdev/answer/pkg/htmltext"
|
||||
|
@ -70,8 +72,6 @@ func (rs *ReportAdminService) ListReportPage(ctx context.Context, dto schema.Get
|
|||
users map[string]*schema.UserBasicInfo
|
||||
)
|
||||
|
||||
pageModel = &pager.PageModel{}
|
||||
|
||||
flags, total, err = rs.reportRepo.GetReportListPage(ctx, dto)
|
||||
if err != nil {
|
||||
return
|
||||
|
@ -139,6 +139,7 @@ func (rs *ReportAdminService) HandleReported(ctx context.Context, req schema.Rep
|
|||
}
|
||||
|
||||
func (rs *ReportAdminService) decorateReportResp(ctx context.Context, resp *schema.GetReportListPageResp) {
|
||||
lang := handler.GetLangByCtx(ctx)
|
||||
objectInfo, err := rs.objectInfoService.GetInfo(ctx, resp.ObjectID)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
|
@ -150,6 +151,7 @@ func (rs *ReportAdminService) decorateReportResp(ctx context.Context, resp *sche
|
|||
resp.CommentID = objectInfo.CommentID
|
||||
resp.Title = objectInfo.Title
|
||||
resp.Excerpt = htmltext.FetchExcerpt(objectInfo.Content, "...", 240)
|
||||
resp.Reason.Translate(configrepo.ID2KeyMapping[resp.ReportType], lang)
|
||||
|
||||
if resp.ReportType > 0 {
|
||||
resp.Reason = &schema.ReasonItem{ReasonType: resp.ReportType}
|
||||
|
@ -164,5 +166,6 @@ func (rs *ReportAdminService) decorateReportResp(ctx context.Context, resp *sche
|
|||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
resp.Reason.Translate(configrepo.ID2KeyMapping[resp.ReportType], lang)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
package service_config
|
||||
|
||||
type ServiceConfig struct {
|
||||
SecretKey string `json:"secret_key" mapstructure:"secret_key" yaml:"secret_key"`
|
||||
UploadPath string `json:"upload_path" mapstructure:"upload_path" yaml:"upload_path"`
|
||||
}
|
||||
|
|
|
@ -39,6 +39,7 @@ func NewSiteInfoService(
|
|||
usersSiteInfo, _ := siteInfoCommonService.GetSiteUsers(context.Background())
|
||||
if usersSiteInfo != nil {
|
||||
constant.DefaultAvatar = usersSiteInfo.DefaultAvatar
|
||||
constant.DefaultGravatarBaseURL = usersSiteInfo.GravatarBaseURL
|
||||
}
|
||||
generalSiteInfo, _ := siteInfoCommonService.GetSiteGeneral(context.Background())
|
||||
if generalSiteInfo != nil {
|
||||
|
@ -229,6 +230,7 @@ func (s *SiteInfoService) SaveSiteUsers(ctx context.Context, req *schema.SiteUse
|
|||
err = s.siteInfoRepo.SaveByType(ctx, constant.SiteTypeUsers, data)
|
||||
if err == nil {
|
||||
constant.DefaultAvatar = req.DefaultAvatar
|
||||
constant.DefaultGravatarBaseURL = req.GravatarBaseURL
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -44,6 +44,9 @@ type TagRepo interface {
|
|||
|
||||
type TagRelRepo interface {
|
||||
AddTagRelList(ctx context.Context, tagList []*entity.TagRel) (err error)
|
||||
RemoveTagRelListByObjectID(ctx context.Context, objectID string) (err error)
|
||||
ShowTagRelListByObjectID(ctx context.Context, objectID string) (err error)
|
||||
HideTagRelListByObjectID(ctx context.Context, objectID string) (err error)
|
||||
RemoveTagRelListByIDs(ctx context.Context, ids []int64) (err error)
|
||||
EnableTagRelByIDs(ctx context.Context, ids []int64) (err error)
|
||||
GetObjectTagRelWithoutStatus(ctx context.Context, objectId, tagID string) (tagRel *entity.TagRel, exist bool, err error)
|
||||
|
@ -653,6 +656,35 @@ func (ts *TagCommonService) RefreshTagQuestionCount(ctx context.Context, tagIDs
|
|||
return nil
|
||||
}
|
||||
|
||||
func (ts *TagCommonService) RefreshTagCountByQuestionID(ctx context.Context, questionID string) (err error) {
|
||||
tagListList, err := ts.tagRelRepo.GetObjectTagRelList(ctx, questionID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tagIDs := make([]string, 0)
|
||||
for _, item := range tagListList {
|
||||
tagIDs = append(tagIDs, item.TagID)
|
||||
}
|
||||
err = ts.RefreshTagQuestionCount(ctx, tagIDs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveTagRelListByObjectID remove tag relation by object id
|
||||
func (ts *TagCommonService) RemoveTagRelListByObjectID(ctx context.Context, objectID string) (err error) {
|
||||
return ts.tagRelRepo.RemoveTagRelListByObjectID(ctx, objectID)
|
||||
}
|
||||
|
||||
func (ts *TagCommonService) HideTagRelListByObjectID(ctx context.Context, objectID string) (err error) {
|
||||
return ts.tagRelRepo.HideTagRelListByObjectID(ctx, objectID)
|
||||
}
|
||||
|
||||
func (ts *TagCommonService) ShowTagRelListByObjectID(ctx context.Context, objectID string) (err error) {
|
||||
return ts.tagRelRepo.ShowTagRelListByObjectID(ctx, objectID)
|
||||
}
|
||||
|
||||
// CreateOrUpdateTagRelList if tag relation is exists update status, if not create it
|
||||
func (ts *TagCommonService) CreateOrUpdateTagRelList(ctx context.Context, objectId string, tagIDs []string) (err error) {
|
||||
addTagIDMapping := make(map[string]bool)
|
||||
|
|
|
@ -20,6 +20,8 @@ type UserRepo interface {
|
|||
AddUser(ctx context.Context, user *entity.User) (err error)
|
||||
IncreaseAnswerCount(ctx context.Context, userID string, amount int) (err error)
|
||||
IncreaseQuestionCount(ctx context.Context, userID string, amount int) (err error)
|
||||
UpdateQuestionCount(ctx context.Context, userID string, count int64) (err error)
|
||||
UpdateAnswerCount(ctx context.Context, userID string, count int) (err error)
|
||||
UpdateLastLoginDate(ctx context.Context, userID string) (err error)
|
||||
UpdateEmailStatus(ctx context.Context, userID string, emailStatus int) error
|
||||
UpdateNoticeStatus(ctx context.Context, userID string, noticeStatus int) error
|
||||
|
@ -30,8 +32,10 @@ type UserRepo interface {
|
|||
GetByUserID(ctx context.Context, userID string) (userInfo *entity.User, exist bool, err error)
|
||||
BatchGetByID(ctx context.Context, ids []string) ([]*entity.User, error)
|
||||
GetByUsername(ctx context.Context, username string) (userInfo *entity.User, exist bool, err error)
|
||||
GetByUsernames(ctx context.Context, usernames []string) ([]*entity.User, error)
|
||||
GetByEmail(ctx context.Context, email string) (userInfo *entity.User, exist bool, err error)
|
||||
GetUserCount(ctx context.Context) (count int64, err error)
|
||||
SearchUserListByName(ctx context.Context, name string) (userList []*entity.User, err error)
|
||||
}
|
||||
|
||||
// UserCommon user service
|
||||
|
@ -72,12 +76,25 @@ func (us *UserCommon) GetUserBasicInfoByUserName(ctx context.Context, username s
|
|||
return info, exist, nil
|
||||
}
|
||||
|
||||
func (us *UserCommon) UpdateAnswerCount(ctx context.Context, userID string, num int) error {
|
||||
return us.userRepo.IncreaseAnswerCount(ctx, userID, num)
|
||||
func (us *UserCommon) BatchGetUserBasicInfoByUserNames(ctx context.Context, usernames []string) (map[string]*schema.UserBasicInfo, error) {
|
||||
infomap := make(map[string]*schema.UserBasicInfo)
|
||||
list, err := us.userRepo.GetByUsernames(ctx, usernames)
|
||||
if err != nil {
|
||||
return infomap, err
|
||||
}
|
||||
for _, user := range list {
|
||||
info := us.FormatUserBasicInfo(ctx, user)
|
||||
infomap[user.Username] = info
|
||||
}
|
||||
return infomap, nil
|
||||
}
|
||||
|
||||
func (us *UserCommon) UpdateQuestionCount(ctx context.Context, userID string, num int) error {
|
||||
return us.userRepo.IncreaseQuestionCount(ctx, userID, num)
|
||||
func (us *UserCommon) UpdateAnswerCount(ctx context.Context, userID string, num int) error {
|
||||
return us.userRepo.UpdateAnswerCount(ctx, userID, num)
|
||||
}
|
||||
|
||||
func (us *UserCommon) UpdateQuestionCount(ctx context.Context, userID string, num int64) error {
|
||||
return us.userRepo.UpdateQuestionCount(ctx, userID, num)
|
||||
}
|
||||
|
||||
func (us *UserCommon) BatchUserBasicInfoByID(ctx context.Context, IDs []string) (map[string]*schema.UserBasicInfo, error) {
|
||||
|
|
|
@ -814,6 +814,19 @@ func (us *UserService) getUserInfoMapping(ctx context.Context, userIDs []string)
|
|||
return userInfoMapping, nil
|
||||
}
|
||||
|
||||
func (us *UserService) SearchUserListByName(ctx context.Context, name string) ([]*schema.UserBasicInfo, error) {
|
||||
userinfolist := make([]*schema.UserBasicInfo, 0)
|
||||
list, err := us.userRepo.SearchUserListByName(ctx, name)
|
||||
if err != nil {
|
||||
return userinfolist, err
|
||||
}
|
||||
for _, user := range list {
|
||||
userinfo := us.userCommonService.FormatUserBasicInfo(ctx, user)
|
||||
userinfolist = append(userinfolist, userinfo)
|
||||
}
|
||||
return userinfolist, nil
|
||||
}
|
||||
|
||||
func (us *UserService) warpStatRankingResp(
|
||||
userInfoMapping map[string]*entity.User,
|
||||
rankStat []*entity.ActivityUserRankStat,
|
||||
|
|
|
@ -195,6 +195,9 @@ func (vs *VoteService) ListUserVotes(ctx context.Context, req schema.GetVoteWith
|
|||
Content: objInfo.Content,
|
||||
VoteType: activity_type.Format(voteInfo.ActivityType),
|
||||
}
|
||||
if objInfo.QuestionStatus == entity.QuestionStatusDeleted {
|
||||
item.Title = "Deleted question"
|
||||
}
|
||||
resp = append(resp, item)
|
||||
}
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@ package checker
|
|||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -13,27 +14,26 @@ const (
|
|||
LevelS
|
||||
)
|
||||
|
||||
// CheckPassword
|
||||
// minLength: Specifies the minimum length of a password
|
||||
// maxLength:Specifies the maximum length of a password
|
||||
// minLevel:Specifies the minimum strength level required for passwords
|
||||
// pwd:Text passwords
|
||||
func CheckPassword(minLength, maxLength, minLevel int, pwd string) error {
|
||||
// First check whether the password length is within the range
|
||||
if len(pwd) < minLength {
|
||||
return fmt.Errorf("BAD PASSWORD: The password is shorter than %d characters", minLength)
|
||||
}
|
||||
if len(pwd) > maxLength {
|
||||
return fmt.Errorf("BAD PASSWORD: The password is logner than %d characters", maxLength)
|
||||
const (
|
||||
PasswordCannotContainSpaces = "error.password.space_invalid"
|
||||
)
|
||||
|
||||
// CheckPassword checks the password strength
|
||||
func CheckPassword(password string) error {
|
||||
if strings.Contains(password, " ") {
|
||||
return fmt.Errorf(PasswordCannotContainSpaces)
|
||||
}
|
||||
|
||||
// TODO Currently there is no requirement for password strength
|
||||
minLevel := 0
|
||||
|
||||
// The password strength level is initialized to D.
|
||||
// The regular is used to verify the password strength.
|
||||
// If the matching is successful, the password strength increases by 1
|
||||
level := levelD
|
||||
patternList := []string{`[0-9]+`, `[a-z]+`, `[A-Z]+`, `[~!@#$%^&*?_-]+`}
|
||||
for _, pattern := range patternList {
|
||||
match, _ := regexp.MatchString(pattern, pwd)
|
||||
match, _ := regexp.MatchString(pattern, password)
|
||||
if match {
|
||||
level++
|
||||
}
|
||||
|
|
|
@ -5,17 +5,15 @@ import (
|
|||
"encoding/hex"
|
||||
"fmt"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultURLPrefix = "https://www.gravatar.com/avatar/"
|
||||
"github.com/answerdev/answer/internal/base/constant"
|
||||
)
|
||||
|
||||
// GetAvatarURL get avatar url from gravatar by email
|
||||
func GetAvatarURL(email string) string {
|
||||
h := md5.New()
|
||||
h.Write([]byte(email))
|
||||
return defaultURLPrefix + hex.EncodeToString(h.Sum(nil))
|
||||
return constant.DefaultGravatarBaseURL + hex.EncodeToString(h.Sum(nil))
|
||||
}
|
||||
|
||||
// Resize resize avatar by pixel
|
||||
|
|
|
@ -4,6 +4,8 @@ import (
|
|||
"fmt"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
)
|
||||
|
||||
func Test_ShortID(t *testing.T) {
|
||||
|
@ -44,3 +46,9 @@ func Test_Demo(t *testing.T) {
|
|||
fmt.Println(num, code)
|
||||
}
|
||||
}
|
||||
|
||||
// https://answer.dev.segmentfault.com/questions/D112
|
||||
func Test_DeCode(t *testing.T) {
|
||||
aaa := DeShortID("D112")
|
||||
spew.Dump(aaa)
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{{define "header"}}
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<html lang="{{.lang}}">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
|
|
Loading…
Reference in New Issue