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:
LinkinStars 2023-05-25 11:22:59 +08:00
commit f16207e30f
73 changed files with 2004 additions and 783 deletions

View File

@ -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

View File

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

View File

@ -4,7 +4,7 @@
# Answer - 构建问答社区
一款问答形式的知识社区开源软件,用来快速构建产品你的产品技术社区、客户支持社区、用户社区等。
一款问答形式的知识社区开源软件,你可以使用它快速建立你的问答社区,用于产品技术支持、客户支持、用户交流等。
了解更多关于该项目的内容,请访问 [answer.dev](https://answer.dev).

View File

@ -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
}

View File

@ -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)

View File

@ -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,

View File

@ -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,

View File

@ -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
View File

@ -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
View File

@ -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=

View File

@ -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 doesnt 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 cant 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:

View File

@ -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:

View File

@ -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"
)

View File

@ -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"
)

View File

@ -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},

View File

@ -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"}]`
)

View File

@ -1,6 +1,7 @@
package constant
var (
DefaultAvatar = "system"
DefaultSiteURL = ""
DefaultAvatar = "system"
DefaultGravatarBaseURL = "https://www.gravatar.com/avatar/"
DefaultSiteURL = ""
)

View File

@ -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"

View File

@ -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
}

View File

@ -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
}

View File

@ -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)

View File

@ -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
}

View File

@ -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) {

View File

@ -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

View File

@ -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)
}

View File

@ -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

View File

@ -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)
}

View File

@ -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"`

View File

@ -4,6 +4,7 @@ import "time"
const (
TagRelStatusAvailable = 1
TagRelStatusHide = 2
TagRelStatusDeleted = 10
)

View File

@ -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 {

View File

@ -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

View File

@ -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)

View File

@ -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)
}

339
internal/migrations/v13.go Normal file
View File

@ -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
}

View File

@ -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)

View File

@ -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}

View File

@ -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
}

View File

@ -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()

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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 {

View File

@ -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

View File

@ -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)
}

View File

@ -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"`

View File

@ -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},

View File

@ -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:"-"`
}

View File

@ -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",
}
)

View File

@ -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)

View File

@ -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)

View File

@ -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 {

View File

@ -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

View File

@ -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)

View File

@ -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"
)

View File

@ -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
}

View File

@ -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
}

View File

@ -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,

View File

@ -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
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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"`
}

View File

@ -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
}

View File

@ -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)

View File

@ -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) {

View File

@ -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,

View File

@ -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)
}

View File

@ -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
// maxLengthSpecifies the maximum length of a password
// minLevelSpecifies the minimum strength level required for passwords
// pwdText 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++
}

View File

@ -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

View File

@ -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)
}

View File

@ -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" />