Merge branch 'dev' into fix/search

This commit is contained in:
kumfo 2022-12-29 19:52:15 +08:00
commit 9444b71bd9
107 changed files with 14392 additions and 682 deletions

View File

@ -39,7 +39,21 @@ builds:
goos:
- linux
goarch:
- arm64
- arm64
- id: build-arm7
main: ./cmd/answer/.
binary: answer
ldflags: -s -w -X main.Version={{.Version}} -X main.Revision={{.ShortCommit}} -X main.Time={{.Date}} -X main.BuildUser=goreleaser
env:
- CC=arm-linux-gnueabihf-gcc
- CXX=arm-linux-gnueabihf-g++
- AR=arm-linux-gnueabihf-ar
goos:
- linux
goarch:
- arm
goarm:
- 7
- id: build-darwin-arm64
main: ./cmd/answer/.
binary: answer

View File

@ -1,6 +1,6 @@
.PHONY: build clean ui
VERSION=1.0.0
VERSION=1.0.1
BIN=answer
DIR_SRC=./cmd/answer
DOCKER_CMD=docker

View File

@ -96,7 +96,7 @@ To run answer, use:
fmt.Println("read config failed: ", err.Error())
return
}
if err = migrations.Migrate(c.Data.Database); err != nil {
if err = migrations.Migrate(c.Data.Database, c.Data.Cache); err != nil {
fmt.Println("migrate failed: ", err.Error())
return
}

View File

@ -14,7 +14,7 @@ import (
"github.com/answerdev/answer/internal/base/translator"
"github.com/answerdev/answer/internal/controller"
"github.com/answerdev/answer/internal/controller/template_render"
"github.com/answerdev/answer/internal/controller_backyard"
"github.com/answerdev/answer/internal/controller_admin"
"github.com/answerdev/answer/internal/repo"
"github.com/answerdev/answer/internal/router"
"github.com/answerdev/answer/internal/service"
@ -38,7 +38,7 @@ func initApplication(
server.ProviderSetServer,
router.ProviderSetRouter,
controller.ProviderSetController,
controller_backyard.ProviderSetController,
controller_admin.ProviderSetController,
templaterender.ProviderSetTemplateRenderController,
service.ProviderSetService,
cron.ProviderSetService,

View File

@ -15,7 +15,7 @@ import (
"github.com/answerdev/answer/internal/base/translator"
"github.com/answerdev/answer/internal/controller"
"github.com/answerdev/answer/internal/controller/template_render"
"github.com/answerdev/answer/internal/controller_backyard"
"github.com/answerdev/answer/internal/controller_admin"
"github.com/answerdev/answer/internal/repo/activity"
"github.com/answerdev/answer/internal/repo/activity_common"
"github.com/answerdev/answer/internal/repo/answer"
@ -61,8 +61,8 @@ import (
rank2 "github.com/answerdev/answer/internal/service/rank"
reason2 "github.com/answerdev/answer/internal/service/reason"
report2 "github.com/answerdev/answer/internal/service/report"
"github.com/answerdev/answer/internal/service/report_backyard"
"github.com/answerdev/answer/internal/service/report_handle_backyard"
"github.com/answerdev/answer/internal/service/report_admin"
"github.com/answerdev/answer/internal/service/report_handle_admin"
"github.com/answerdev/answer/internal/service/revision_common"
role2 "github.com/answerdev/answer/internal/service/role"
"github.com/answerdev/answer/internal/service/search_parser"
@ -72,7 +72,7 @@ import (
tag2 "github.com/answerdev/answer/internal/service/tag"
tag_common2 "github.com/answerdev/answer/internal/service/tag_common"
"github.com/answerdev/answer/internal/service/uploader"
"github.com/answerdev/answer/internal/service/user_backyard"
"github.com/answerdev/answer/internal/service/user_admin"
"github.com/answerdev/answer/internal/service/user_common"
"github.com/segmentfault/pacman"
"github.com/segmentfault/pacman/log"
@ -177,18 +177,18 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database,
revisionController := controller.NewRevisionController(serviceRevisionService, rankService)
rankController := controller.NewRankController(rankService)
commonRepo := common.NewCommonRepo(dataData, uniqueIDRepo)
reportHandle := report_handle_backyard.NewReportHandle(questionCommon, commentRepo, configRepo)
reportBackyardService := report_backyard.NewReportBackyardService(reportRepo, userCommon, commonRepo, answerRepo, questionRepo, commentCommonRepo, reportHandle, configRepo)
controller_backyardReportController := controller_backyard.NewReportController(reportBackyardService)
userBackyardRepo := user.NewUserBackyardRepo(dataData, authRepo)
userBackyardService := user_backyard.NewUserBackyardService(userBackyardRepo, userRoleRelService, authService, userCommon)
userBackyardController := controller_backyard.NewUserBackyardController(userBackyardService)
reportHandle := report_handle_admin.NewReportHandle(questionCommon, commentRepo, configRepo)
reportAdminService := report_admin.NewReportAdminService(reportRepo, userCommon, commonRepo, answerRepo, questionRepo, commentCommonRepo, reportHandle, configRepo)
controller_adminReportController := controller_admin.NewReportController(reportAdminService)
userAdminRepo := user.NewUserAdminRepo(dataData, authRepo)
userAdminService := user_admin.NewUserAdminService(userAdminRepo, userRoleRelService, authService, userCommon)
userAdminController := controller_admin.NewUserAdminController(userAdminService)
reasonRepo := reason.NewReasonRepo(configRepo)
reasonService := reason2.NewReasonService(reasonRepo)
reasonController := controller.NewReasonController(reasonService)
themeController := controller_backyard.NewThemeController()
themeController := controller_admin.NewThemeController()
siteInfoService := siteinfo.NewSiteInfoService(siteInfoRepo, siteInfoCommonService, emailService, tagCommonService)
siteInfoController := controller_backyard.NewSiteInfoController(siteInfoService)
siteInfoController := controller_admin.NewSiteInfoController(siteInfoService)
siteinfoController := controller.NewSiteinfoController(siteInfoCommonService)
notificationRepo := notification.NewNotificationRepo(dataData)
notificationCommon := notificationcommon.NewNotificationCommon(dataData, notificationRepo, userCommon, activityRepo, followRepo, objService)
@ -201,8 +201,8 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database,
commentCommonService := comment_common.NewCommentCommonService(commentCommonRepo)
activityService := activity2.NewActivityService(activityActivityRepo, userCommon, activityCommon, tagCommonService, objService, commentCommonService, revisionService, metaService)
activityController := controller.NewActivityController(activityCommon, activityService)
roleController := controller_backyard.NewRoleController(roleService)
answerAPIRouter := router.NewAnswerAPIRouter(langController, userController, commentController, reportController, voteController, tagController, followController, collectionController, questionController, answerController, searchController, revisionController, rankController, controller_backyardReportController, userBackyardController, reasonController, themeController, siteInfoController, siteinfoController, notificationController, dashboardController, uploadController, activityController, roleController)
roleController := controller_admin.NewRoleController(roleService)
answerAPIRouter := router.NewAnswerAPIRouter(langController, userController, commentController, reportController, voteController, tagController, followController, collectionController, questionController, answerController, searchController, revisionController, rankController, controller_adminReportController, userAdminController, reasonController, themeController, siteInfoController, siteinfoController, notificationController, dashboardController, uploadController, activityController, roleController)
swaggerRouter := router.NewSwaggerRouter(swaggerConf)
uiRouter := router.NewUIRouter(siteinfoController, siteInfoCommonService)
authUserMiddleware := middleware.NewAuthUserMiddleware(authService, siteInfoCommonService)

View File

@ -33,7 +33,7 @@ const docTemplate = `{
"tags": [
"admin"
],
"summary": "CmsSearchList",
"summary": "AdminSearchAnswerList",
"parameters": [
{
"type": "integer",
@ -184,7 +184,7 @@ const docTemplate = `{
"tags": [
"admin"
],
"summary": "CmsSearchList",
"summary": "AdminSearchList",
"parameters": [
{
"type": "integer",
@ -1671,7 +1671,7 @@ const docTemplate = `{
"ApiKeyAuth": []
}
],
"description": "Adopted",
"description": "Accepted",
"consumes": [
"application/json"
],
@ -1681,15 +1681,15 @@ const docTemplate = `{
"tags": [
"api-answer"
],
"summary": "Adopted",
"summary": "Accepted",
"parameters": [
{
"description": "AnswerAdoptedReq",
"description": "AnswerAcceptedReq",
"name": "data",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/schema.AnswerAdoptedReq"
"$ref": "#/definitions/schema.AnswerAcceptedReq"
}
}
],
@ -5490,6 +5490,17 @@ const docTemplate = `{
}
}
},
"schema.AnswerAcceptedReq": {
"type": "object",
"properties": {
"answer_id": {
"type": "string"
},
"question_id": {
"type": "string"
}
}
},
"schema.AnswerAddReq": {
"type": "object",
"properties": {
@ -5507,17 +5518,6 @@ const docTemplate = `{
}
}
},
"schema.AnswerAdoptedReq": {
"type": "object",
"properties": {
"answer_id": {
"type": "string"
},
"question_id": {
"type": "string"
}
}
},
"schema.AnswerUpdateReq": {
"type": "object",
"properties": {
@ -7011,7 +7011,6 @@ const docTemplate = `{
"type": "object",
"required": [
"language",
"theme",
"time_zone"
],
"properties": {
@ -7019,10 +7018,6 @@ const docTemplate = `{
"type": "string",
"maxLength": 128
},
"theme": {
"type": "string",
"maxLength": 128
},
"time_zone": {
"type": "string",
"maxLength": 128
@ -7033,7 +7028,6 @@ const docTemplate = `{
"type": "object",
"required": [
"language",
"theme",
"time_zone"
],
"properties": {
@ -7041,10 +7035,6 @@ const docTemplate = `{
"type": "string",
"maxLength": 128
},
"theme": {
"type": "string",
"maxLength": 128
},
"time_zone": {
"type": "string",
"maxLength": 128

View File

@ -21,7 +21,7 @@
"tags": [
"admin"
],
"summary": "CmsSearchList",
"summary": "AdminSearchAnswerList",
"parameters": [
{
"type": "integer",
@ -172,7 +172,7 @@
"tags": [
"admin"
],
"summary": "CmsSearchList",
"summary": "AdminSearchList",
"parameters": [
{
"type": "integer",
@ -1659,7 +1659,7 @@
"ApiKeyAuth": []
}
],
"description": "Adopted",
"description": "Accepted",
"consumes": [
"application/json"
],
@ -1669,15 +1669,15 @@
"tags": [
"api-answer"
],
"summary": "Adopted",
"summary": "Accepted",
"parameters": [
{
"description": "AnswerAdoptedReq",
"description": "AnswerAcceptedReq",
"name": "data",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/schema.AnswerAdoptedReq"
"$ref": "#/definitions/schema.AnswerAcceptedReq"
}
}
],
@ -5478,6 +5478,17 @@
}
}
},
"schema.AnswerAcceptedReq": {
"type": "object",
"properties": {
"answer_id": {
"type": "string"
},
"question_id": {
"type": "string"
}
}
},
"schema.AnswerAddReq": {
"type": "object",
"properties": {
@ -5495,17 +5506,6 @@
}
}
},
"schema.AnswerAdoptedReq": {
"type": "object",
"properties": {
"answer_id": {
"type": "string"
},
"question_id": {
"type": "string"
}
}
},
"schema.AnswerUpdateReq": {
"type": "object",
"properties": {
@ -6999,7 +6999,6 @@
"type": "object",
"required": [
"language",
"theme",
"time_zone"
],
"properties": {
@ -7007,10 +7006,6 @@
"type": "string",
"maxLength": 128
},
"theme": {
"type": "string",
"maxLength": 128
},
"time_zone": {
"type": "string",
"maxLength": 128
@ -7021,7 +7016,6 @@
"type": "object",
"required": [
"language",
"theme",
"time_zone"
],
"properties": {
@ -7029,10 +7023,6 @@
"type": "string",
"maxLength": 128
},
"theme": {
"type": "string",
"maxLength": 128
},
"time_zone": {
"type": "string",
"maxLength": 128

View File

@ -207,6 +207,13 @@ definitions:
status:
type: string
type: object
schema.AnswerAcceptedReq:
properties:
answer_id:
type: string
question_id:
type: string
type: object
schema.AnswerAddReq:
properties:
content:
@ -219,13 +226,6 @@ definitions:
description: question_id
type: string
type: object
schema.AnswerAdoptedReq:
properties:
answer_id:
type: string
question_id:
type: string
type: object
schema.AnswerUpdateReq:
properties:
content:
@ -1302,15 +1302,11 @@ definitions:
language:
maxLength: 128
type: string
theme:
maxLength: 128
type: string
time_zone:
maxLength: 128
type: string
required:
- language
- theme
- time_zone
type: object
schema.SiteInterfaceResp:
@ -1318,15 +1314,11 @@ definitions:
language:
maxLength: 128
type: string
theme:
maxLength: 128
type: string
time_zone:
maxLength: 128
type: string
required:
- language
- theme
- time_zone
type: object
schema.SiteLegalReq:
@ -1932,7 +1924,7 @@ paths:
$ref: '#/definitions/handler.RespBody'
security:
- ApiKeyAuth: []
summary: CmsSearchList
summary: AdminSearchAnswerList
tags:
- admin
/answer/admin/api/answer/status:
@ -2024,7 +2016,7 @@ paths:
$ref: '#/definitions/handler.RespBody'
security:
- ApiKeyAuth: []
summary: CmsSearchList
summary: AdminSearchList
tags:
- admin
/answer/admin/api/question/status:
@ -2889,14 +2881,14 @@ paths:
post:
consumes:
- application/json
description: Adopted
description: Accepted
parameters:
- description: AnswerAdoptedReq
- description: AnswerAcceptedReq
in: body
name: data
required: true
schema:
$ref: '#/definitions/schema.AnswerAdoptedReq'
$ref: '#/definitions/schema.AnswerAcceptedReq'
produces:
- application/json
responses:
@ -2906,7 +2898,7 @@ paths:
type: string
security:
- ApiKeyAuth: []
summary: Adopted
summary: Accepted
tags:
- api-answer
/answer/api/v1/answer/info:

7
go.mod
View File

@ -27,10 +27,10 @@ require (
github.com/mojocn/base64Captcha v1.3.5
github.com/ory/dockertest/v3 v3.9.1
github.com/robfig/cron/v3 v3.0.1
github.com/segmentfault/pacman v1.0.1
github.com/segmentfault/pacman/contrib/cache/memory v0.0.0-20221018072427-a15dd1434e05
github.com/segmentfault/pacman v1.0.2
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-20221207032920-3662d1e32068
github.com/segmentfault/pacman/contrib/i18n v0.0.0-20221219081300-f734f4a16aa0
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
@ -38,6 +38,7 @@ require (
github.com/swaggo/files v0.0.0-20220728132757-551d4a08d97a
github.com/swaggo/gin-swagger v1.5.3
github.com/swaggo/swag v1.8.7
github.com/yuin/goldmark v1.4.13
golang.org/x/crypto v0.1.0
golang.org/x/net v0.1.0
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df

13
go.sum
View File

@ -599,14 +599,14 @@ github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/seccomp/libseccomp-golang v0.9.2-0.20210429002308-3879420cc921/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg=
github.com/segmentfault/pacman v1.0.1 h1:GFdvPtNxvVVjnDM4ty02D/+4unHwG9PmjcOZSc2wRXE=
github.com/segmentfault/pacman v1.0.1/go.mod h1:5lNp5REd8QMThmBUvR3Fi9Y3AsOB4GRq7soCB4QLqOs=
github.com/segmentfault/pacman/contrib/cache/memory v0.0.0-20221018072427-a15dd1434e05 h1:rXsXgC/HR7m4V425l9pDBW/qxJv6zCh6pEvvO1ZCNsI=
github.com/segmentfault/pacman/contrib/cache/memory v0.0.0-20221018072427-a15dd1434e05/go.mod h1:rmf1TCwz67dyM+AmTwSd1BxTo2AOYHj262lP93bOZbs=
github.com/segmentfault/pacman v1.0.2 h1:tXWkEzePiSVQXYwFH3tOuxC1/DJ5ISi35F93lKNGs3o=
github.com/segmentfault/pacman v1.0.2/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-20221207032920-3662d1e32068 h1:ln/qgrC62e7/XHGPiikWFV4dyYgCaWeZYkmSGqrHZp4=
github.com/segmentfault/pacman/contrib/i18n v0.0.0-20221207032920-3662d1e32068/go.mod h1:7QcRmnV7OYq4hNOOCWXT5HXnN/u756JUsqIW0Bw8n9E=
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/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=
@ -693,6 +693,7 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0=

1336
i18n/de_DE.yaml Normal file

File diff suppressed because it is too large Load Diff

View File

@ -210,7 +210,7 @@ backend:
other: "answered question"
update_answer:
other: "updated answer"
adopt_answer:
accept_answer:
other: "accepted answer"
comment_question:
other: "commented question"

1336
i18n/es_ES.yaml Normal file

File diff suppressed because it is too large Load Diff

1336
i18n/fr_FR.yaml Normal file

File diff suppressed because it is too large Load Diff

View File

@ -2,5 +2,25 @@
language_options:
- label: "简体中文(CN)"
value: "zh_CN"
- label: "繁體中文(CN)"
value: "zh_TW"
- label: "English(US)"
value: "en_US"
- label: "Deutsch(DE)"
value: "de_DE"
- label: "Español(ES)"
value: "es_ES"
- label: "Français(FR)"
value: "fr_FR"
- label: "Italiano(IT)"
value: "it_IT"
- label: "日本語(JA)"
value: "ja_JP"
- label: "한국어(KO)"
value: "ko_KR"
- label: "Português(PT)"
value: "pt_PT"
- label: "Русский(RU)"
value: "ru_RU"
- label: "Tiếng Việt(VI)"
value: "vi_VN"

File diff suppressed because it is too large Load Diff

1336
i18n/ja_JP.yaml Normal file

File diff suppressed because it is too large Load Diff

1336
i18n/ko_KR.yaml Normal file

File diff suppressed because it is too large Load Diff

1336
i18n/pt_PT.yaml Normal file

File diff suppressed because it is too large Load Diff

1336
i18n/ru_RU.yaml Normal file

File diff suppressed because it is too large Load Diff

1336
i18n/vi_VN.yaml Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,3 +1,4 @@
#The following fields are used for back-end
backend:
base:
success:
@ -10,7 +11,6 @@ backend:
other: "未登录"
database_error:
other: "数据服务异常"
role:
name:
user:
@ -26,21 +26,23 @@ backend:
other: "拥有进入网站的全部权限。"
moderator:
other: "有权访问所有的帖子,无法进入管理员设置页面。"
email:
other: "邮箱"
password:
other: "密码"
email_or_password_wrong_error: &email_or_password_wrong
email_or_password_wrong_error:
other: "邮箱或密码错误"
error:
admin:
email_or_password_wrong: *email_or_password_wrong
email_or_password_wrong:
other: 邮箱或密码错误
answer:
not_found:
other: "答案未找到"
cannot_deleted:
other: "无删除权限"
cannot_update:
other: "无修改权限"
comment:
edit_without_permission:
other: "不允许编辑评论"
@ -78,6 +80,12 @@ backend:
question:
not_found:
other: "问题未找到"
cannot_deleted:
other: "无删除权限"
cannot_close:
other: "无关闭权限"
cannot_update:
other: "无更新权限"
rank:
fail_to_meet_the_condition:
other: "级别不符合条件"
@ -102,9 +110,15 @@ backend:
theme:
not_found:
other: "主题未找到"
revision:
review_underway:
other: "目前无法编辑,有一个版本在审阅队列中。"
no_permission:
other: "无权限修改"
user:
email_or_password_wrong:
other: *email_or_password_wrong
other:
other: 邮箱或密码错误
not_found:
other: "用户未找到"
suspended:
@ -115,71 +129,75 @@ backend:
other: "用户名已被使用"
set_avatar:
other: "头像设置错误"
config:
read_config_failed:
other: "读取配置失败"
database:
connection_failed:
other: "数据连接异常!"
create_table_failed:
other: "创建表失败"
install:
create_config_failed:
other: "无法创建配置文件"
cannot_update_your_role:
other: "你无法修改自己的角色"
not_allowed_registration:
other: "目前该网站尚未开放注册"
revision:
review_underway:
other: "目前无法编辑,有一个版本在审阅队列中。"
no_permission:
other: "无权限修改"
report:
spam:
name:
other: "垃圾信息"
description:
other: "此帖子是一个广告贴,或是破坏性行为。它对当前的主题无用,也不相关。"
desc:
other: "This post is an advertisement, or vandalism. It is not useful or relevant to the current topic."
rude:
name:
other: "粗鲁或辱骂的"
description:
other: "有理智的人都会发现此内容不适合进行尊重的讨论。"
desc:
other: "A reasonable person would find this content inappropriate for respectful discourse."
duplicate:
name:
other: "重复信息"
description:
other: "此问题以前就有人问过,而且已经有了答案。"
desc:
other: "This question has been asked before and already has an answer."
not_answer:
name:
other: "不是答案"
description:
other: "此帖子是作为一个答案发布的,但它并没有试图回答这个问题。总之,它可能应该是个编辑,评论,另一个问题或者被删除。"
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:
name:
other: "不再需要"
description:
other: "此条评论是过时的,对话性的或与本帖无关。"
desc:
other: "This comment is outdated, conversational or not relevant to this post."
other:
name:
other: "其他原因"
description:
other: "此帖子需要工作人员关注,因为是上述所列以外的其他理由。"
desc:
other: "This post requires staff attention for another reason not listed above."
question:
close:
duplicate:
name:
other: "垃圾信息"
description:
other: "此问题以前就有人问过,而且已经有了答案。"
desc:
other: "This question has been asked before and already has an answer."
guideline:
name:
other: "社区特定原因"
description:
other: "此问题不符合社区准则。"
desc:
other: "This question doesn't meet a community guideline."
multiple:
name:
other: "需要细节或澄清"
description:
other: "此问题目前涵盖多个问题。它应该只集中在一个问题上。"
desc:
other: "This question currently includes multiple questions in one. It should focus on one problem only."
other:
name:
other: "其他原因"
description:
other: "此帖子需要上述所列以外的其他理由。"
desc:
other: "This post requires another reason not listed above."
notification:
action:
update_question:
@ -188,7 +206,7 @@ backend:
other: "回答了问题"
update_answer:
other: "更新了答案"
adopt_answer:
accept_answer:
other: "接受了答案"
comment_question:
other: "评论了问题"
@ -206,7 +224,7 @@ backend:
other: "你的答案已被删除"
your_comment_was_deleted:
other: "你的评论已被删除"
# The following fields are used for interface presentation(Front-end)
#The following fields are used for interface presentation(Front-end)
ui:
how_to_format:
title: 如何设定文本格式
@ -244,6 +262,11 @@ ui:
confirm_email: 确认电子邮件
account_suspended: 账号已封禁
admin: 后台管理
change_email: 修改邮箱
install: Answer 安装
upgrade: Answer 升级
maintenance: 网站维护
users: Users
notifications:
title: 通知
inbox: 收件箱
@ -252,7 +275,7 @@ ui:
show_more: 显示更多
suspended:
title: 账号已封禁
until_time: '你的账号被封禁至{{ time }}。'
until_time: "你的账号被封禁至{{ time }}。"
forever: 你的账号已被永久封禁。
end: 违反了我们的社区准则。
editor:
@ -399,6 +422,7 @@ ui:
tag_info:
created_at: 创建于
edited_at: 编辑于
history: 历史
synonyms:
title: 同义词
text: 以下标签等同于
@ -409,7 +433,8 @@ ui:
synonyms_text: 以下标签等同于
delete:
title: 删除标签
content: <p>不允许删除有关联问题的标签。</p><p>请先从关联的问题中删除此标签的引用。</p>
content: >-
<p>不允许删除有关联问题的标签。</p><p>请先从关联的问题中删除此标签的引用。</p>
content2: 确定要删除吗?
close: 关闭
edit_tag:
@ -428,17 +453,20 @@ ui:
label: 描述
edit_summary:
label: 编辑概要
placeholder: 简单描述更改原因 (错别字、文字表达、格式等等)
placeholder: >-
简单描述更改原因 (错别字、文字表达、格式等等)
btn_save_edits: 保存更改
btn_cancel: 取消
dates:
long_date: MM月DD日
long_date_with_year: YYYY年MM月DD日
long_date_with_time: 'YYYY年MM月DD日 HH:mm'
long_date_with_year: "YYYY年MM月DD日"
long_date_with_time: "YYYY年MM月DD日 HH:mm"
now: 刚刚
x_seconds_ago: '{{count}} 秒前'
x_minutes_ago: '{{count}} 分钟前'
x_hours_ago: '{{count}} 小时前'
x_seconds_ago: "{{count}} 秒前"
x_minutes_ago: "{{count}} 分钟前"
x_hours_ago: "{{count}} 小时前"
hour: 小时
day:
comment:
btn_add_comment: 添加评论
reply_to: 回复
@ -449,8 +477,10 @@ ui:
btn_save_edits: 保存
btn_cancel: 取消
show_more: 显示更多评论
tip_question: 使用评论提问更多信息或者提出改进意见。尽量避免使用评论功能回答问题。
tip_answer: 使用评论对回答者进行回复,或者通知回答者你已更新了问题的内容。如果要补充或者完善问题的内容,请在原问题中更改。
tip_question: >-
使用评论提问更多信息或者提出改进意见。尽量避免使用评论功能回答问题。
tip_answer: >-
使用评论对回答者进行回复,或者通知回答者你已更新了问题的内容。如果要补充或者完善问题的内容,请在原问题中更改。
edit_answer:
title: 编辑回答
default_reason: 编辑回答
@ -462,7 +492,8 @@ ui:
label: 回答内容
edit_summary:
label: 编辑概要
placeholder: 简单描述更改原因 (错别字、文字表达、格式等等)
placeholder: >-
简单描述更改原因 (错别字、文字表达、格式等等)
btn_save_edits: 保存更改
btn_cancel: 取消
tags:
@ -504,6 +535,10 @@ ui:
label: 回答内容
msg:
empty: 回答内容不能为空
edit_summary:
label: 编辑理由
placeholder: >-
简单描述更改原因 (错别字、文字表达、格式等等)
btn_post_question: 提交问题
btn_save_edits: 保存更改
answer_question: 直接发表回答
@ -512,8 +547,9 @@ ui:
add_btn: 添加标签
create_btn: 创建新标签
search_tag: 搜索标签
hint: 选择至少一个与问题相关的标签。
hint: "选择至少一个与问题相关的标签。"
no_result: 没有匹配的标签
tag_required_text: 必填标签 (至少一个)
header:
nav:
question: 问题
@ -523,6 +559,7 @@ ui:
setting: 账号设置
logout: 退出登录
admin: 后台管理
review: 审查
search:
placeholder: 搜索
footer:
@ -538,16 +575,20 @@ ui:
msg:
empty: 不能为空
inactive:
first: '马上就好了!我们发送了一封激活邮件到 <bold>{{mail}}</bold>。请按照邮件中的说明激活您的帐户。'
info: 如果没有收到,请检查您的垃圾邮件文件夹。
another: '我们向您发送了另一封激活电子邮件,地址为 <bold>{{mail}}</bold>。它可能需要几分钟才能到达;请务必检查您的垃圾邮件文件夹。'
first: >-
就差一步!我们发送了一封激活邮件到 <bold>{{mail}}</bold>。请按照邮件中的说明激活您的账户。
info: "如果没有收到,请检查您的垃圾邮件文件夹。"
another: >-
我们向您发送了另一封激活电子邮件,地址为 <bold>{{mail}}</bold>。它可能需要几分钟才能到达;请务必检查您的垃圾邮件文件夹。
btn_name: 重新发送激活邮件
change_btn_name: 更改邮箱
msg:
empty: 不能为空
login:
page_title: 欢迎来到 Answer
info_sign: 没有帐户?<1>注册</1>
info_login: 已经有一个帐户?<1>登录</1>
info_sign: 没有账户?<1>注册</1>
info_login: 已经有一个账户?<1>登录</1>
agreements: 登录即表示您同意<1>隐私政策</1>和<3>服务条款</3>。
forgot_pass: 忘记密码?
name:
label: 昵称
@ -566,16 +607,29 @@ ui:
account_forgot:
page_title: 忘记密码
btn_name: 发送恢复邮件
send_success: '如无意外,你的邮箱 <strong>{{mail}}</strong> 将会收到一封重置密码的邮件,请根据指引重置你的密码。'
send_success: >-
如无意外,你的邮箱 <strong>{{mail}}</strong> 将会收到一封重置密码的邮件,请根据指引重置你的密码。
email:
label: 邮箱
msg:
empty: 邮箱不能为空
change_email:
page_title: 欢迎来到 Answer
btn_cancel: 取消
btn_update: 更新电子邮件地址
send_success: >-
If an account matches <strong>{{mail}}</strong>, you should receive an email with instructions on how to reset your password shortly.
email:
label: 新邮箱
msg:
empty: 邮箱不能为空
password_reset:
page_title: 密码重置
btn_name: 重置我的密码
reset_success: 你已经成功更改密码,将返回登录页面
link_invalid: 抱歉,此密码重置链接已失效。也许是你已经重置过密码了?
reset_success: >-
你已经成功更改密码,将返回登录页面
link_invalid: >-
抱歉,此密码重置链接已失效。也许是你已经重置过密码了?
to_login: 前往登录页面
password:
label: 密码
@ -593,7 +647,8 @@ ui:
account: 账号
interface: 界面
profile:
btn_name: 保存更改
heading: Profile
btn_name: Save
display_name:
label: 昵称
msg: 昵称不能为空
@ -606,24 +661,33 @@ ui:
character: '用户名只能由 "a-z", "0-9", " - . _" 组成'
avatar:
label: 头像
text: 您可以上传图片作为头像。
gravatar: Gravatar
gravatar_text: 您可以在 <1>gravatar.com</1> 更改图像
custom: 自定义
btn_refresh: 刷新
custom_text: 您可以上传您的图片。
default: System
msg: 请上传头像
bio:
label: 关于我 (可选)
website:
label: 网站 (可选)
placeholder: 'https://example.com'
placeholder: "https://example.com"
msg: 格式不正确
location:
label: 位置 (可选)
placeholder: '城市, 国家'
placeholder: "城市, 国家"
notification:
heading: Notifications
email:
label: 邮件通知
radio: 你的提问有新的回答,评论,和其他
radio: "你的提问有新的回答,评论,和其他"
account:
heading: Account
change_email_btn: 更改邮箱
change_pass_btn: 更改密码
change_email_info: 邮件已发送。请根据指引完成验证。
change_email_info: >-
邮件已发送。请根据指引完成验证。
email:
label: 邮箱
msg: 邮箱不能为空
@ -639,6 +703,7 @@ ui:
pass_confirm:
label: 确认新密码
interface:
heading: Interface
lang:
label: 界面语言
text: 设置用户界面语言,在刷新页面后生效。
@ -646,6 +711,8 @@ ui:
update: 更新成功
update_password: 更改密码成功。
flag_success: 感谢您的标记,我们会尽快处理。
forbidden_operate_self: Forbidden to operate on yourself
review: 您的修订将在审核通过后显示。
related_question:
title: 相关问题
btn: 我要提问
@ -670,10 +737,16 @@ ui:
write_answer:
title: 你的回答
btn_name: 提交你的回答
add_another_answer: Add another answer
confirm_title: 继续回答
continue: 继续
confirm_info: <p>您确定要提交一个新的回答吗?</p><p>您可以直接编辑和改善您之前的回答的。</p>
confirm_info: >-
<p>您确定要提交一个新的回答吗?</p><p>您可以直接编辑和改善您之前的回答的。</p>
empty: 回答内容不能为空。
reopen:
title: Reopen this post
content: Are you sure you want to reopen?
success: This post has been reopened
delete:
title: 删除
question: >-
@ -693,27 +766,31 @@ ui:
logout: 退出登录
verify: 验证
add_question: 我要提问
approve: 批准
reject: 拒绝
skip: 略过
search:
title: 搜索结果
keywords: 关键词
options: 选项
follow: 关注
following: 已关注
counts: '{{count}} 个结果'
counts: "{{count}} 个结果"
more: 更多
sort_btns:
relevance: 相关性
newest: 最新的
active: 活跃的
score: 评分
more: 更多
tips:
title: 高级搜索提示
tag: '<1>[tag]</1> 在指定标签中搜索'
user: '<1>user:username</1> 根据作者搜索'
answer: '<1>answers:0</1> 搜索未回答的问题'
score: '<1>score:3</1> 评分 3 分或以上'
question: '<1>is:question</1> 只搜索问题'
is_answer: '<1>is:answer</1> 只搜索回答'
tag: "<1>[tag]</1> 在指定标签中搜索"
user: "<1>user:username</1> 根据作者搜索"
answer: "<1>answers:0</1> 搜索未回答的问题"
score: "<1>score:3</1> 评分 3 分或以上"
question: "<1>is:question</1> 只搜索问题"
is_answer: "<1>is:answer</1> 只搜索回答"
empty: 找不到任何相关的内容。<br /> 请尝试其他关键字,或者减少查找内容的长度。
share:
name: 分享
@ -729,9 +806,11 @@ ui:
page_title: 欢迎来到 Answer
success: 你的账号已通过验证,即将返回首页。
link: 返回首页
invalid: 抱歉,此验证链接已失效。也许是你的账号已经通过验证了?
invalid: >-
抱歉,此验证链接已失效。也许是你的账号已经通过验证了?
confirm_new_email: 你的电子邮箱已更新
confirm_new_email_invalid: 抱歉,此验证链接已失效。也许是你的邮箱已经成功更改了?
confirm_new_email_invalid: >-
抱歉,此验证链接已失效。也许是你的邮箱已经成功更改了?
question:
following_tags: 已关注的标签
edit: 编辑
@ -739,8 +818,8 @@ ui:
follow_tag_tip: 按照标签整理您的问题列表。
hot_questions: 热点问题
all_questions: 全部问题
x_questions: '{{ count }} 个问题'
x_answers: '{{ count }} 个回答'
x_questions: "{{ count }} 个问题"
x_answers: "{{ count }} 个回答"
questions: 个问题
answers: 回答
newest: 最新
@ -767,12 +846,12 @@ ui:
newest: 最新
score: 评分
edit_profile: 编辑我的资料
visited_x_days: 'Visited {{ count }} days'
viewed: Viewed
visited_x_days: "已访问 {{ count }} 天"
viewed: 阅读次数
joined: 加入于
last_login: 上次登录
about_me: 关于我
about_me_empty: '// Hello, World !'
about_me_empty: "Hello, World!"
top_answers: 热门回答
top_questions: 热门问题
stats: 状态
@ -788,12 +867,94 @@ ui:
x_votes: 得票
x_answers: 个回答
x_questions: 个问题
install:
title: Answer
next: 下一步
done: 完成
config_yaml_error: 无法创建配置文件
lang:
label: Please Choose a Language
db_type:
label: Database Engine
db_username:
label: 用户名
placeholder: root
msg: 用户名不能为空
db_password:
label: 密码
placeholder: root
msg: 密码不能为空
db_host:
label: 数据库服务器地址
placeholder: "db: 3306"
msg: 数据库地址不能为空
db_name:
label: 数据库名
placeholder: answer
msg: 数据库名称不能为空。
db_file:
label: Database File
placeholder: /data/answer.db
msg: 数据库文件不能为空。
config_yaml:
title: 创建 config.yaml
label: 已创建 config.yaml 文件。
desc: >-
You can create the <1>config.yaml</1> file manually in the <1>/var/wwww/xxx/</1> directory and paste the following text into it.
info: "完成后,点击“下一步”按钮。"
site_information: 站点信息
admin_account: 管理员账户
site_name:
label: 站点名称
msg: 站点名称不能为空。
site_url:
label: 站点地址URL
text: The address of your site.
msg:
empty: 站点URL不能为空。
incorrect: 站点URL格式不正确。
contact_email:
label: 联系邮箱
text: Email address of key contact responsible for this site.
msg:
empty: Contact Email cannot be empty.
incorrect: Contact Email incorrect format.
admin_name:
label: Name
msg: Name cannot be empty.
admin_password:
label: Password
text: >-
You will need this password to log in. Please store it in a secure location.
msg: Password cannot be empty.
admin_email:
label: Email
text: You will need this email to log in.
msg:
empty: Email cannot be empty.
incorrect: Email incorrect format.
ready_title: Your Answer is Ready!
ready_desc: >-
If you ever feel like changing more settings, visit <1>admin section</1>; find it in the site menu.
good_luck: "Have fun, and good luck!"
warn_title: Warning
warn_desc: >-
The file <1>config.yaml</1> already exists. If you need to reset any of the configuration items in this file, please delete it first.
install_now: You may try <1>installing now</1>.
installed: 已安裝
installed_desc: >-
You appear to have already installed. To reinstall please clear your old database tables first.
db_failed: 数据连接异常!
db_failed_desc: >-
This either means that the database information in your <1>config.yaml</1> file is incorrect or that contact with the database server could not be established. This could mean your hosts database server is down.
page_404:
desc: 页面不存在
back_home: 回到主页
page_50X:
desc: 服务器遇到了一个错误,无法完成你的请求。
back_home: 回到主页
page_maintenance:
desc: "We are under maintenance, well be back soon."
nav_menus:
dashboard: 后台管理
contents: 内容管理
@ -805,13 +966,52 @@ ui:
general: 一般
interface: 界面
smtp: SMTP
branding: 品牌
legal: 法律条款
write: 撰写
tos: 服务条款
privacy: 隐私政策
seo: SEO
customize: Customize
themes: Themes
css-html: CSS/HTML
login: Login
admin:
admin_header:
title: 后台管理
dashboard:
title: 后台管理
welcome: 欢迎来到 Answer 后台管理!
version: 版本
site_statistics: 站点统计
questions: "问题:"
answers: "回答:"
comments: "评论:"
votes: "投票:"
active_users: "活跃用户:"
flags: "举报:"
site_health_status: '健康状态:'
version: "版本"
https: "HTTPS:"
uploading_files: "上传文件:"
smtp: "SMTP:"
timezone: "时区:"
system_info: 系统信息
storage_used: "已用存储空间:"
uptime: "运行时间:"
answer_links: 回答链接
documents: 文档
feedback: 用户反馈
review: 审查
config: 配置
update_to: 更新到
latest: 最新版本
check_failed: 校验失败
"yes": "是"
"no": "否"
not_allowed: 拒绝
allowed: 允许
enabled: 已启用
disabled: 停用
flags:
title: 举报
pending: 等待处理
@ -838,7 +1038,7 @@ ui:
msg:
empty: 请选择一个原因
status_modal:
title: '更改 {{ type }} 状态为...'
title: "更改 {{ type }} 状态为..."
normal_name: 正常
normal_desc: 所有用户都可以访问
closed_name: 关闭
@ -848,6 +1048,10 @@ ui:
btn_cancel: 取消
btn_submit: 提交
btn_next: 下一步
user_role_modal:
title: 更改用户状态为...
btn_cancel: 取消
btn_submit: 提交
users:
title: 用户
name: 名称
@ -857,13 +1061,50 @@ ui:
delete_at: 删除时间
suspend_at: 封禁时间
status: 状态
role: 角色
action: 操作
change: 更改
all: 全部
staff: 工作人员
inactive: 不活跃
suspended: 已封禁
deleted: 已删除
normal: 正常
Moderator: 版主
Admin: 管理员
User: 用户
filter:
placeholder: "按名称筛选用户id"
set_new_password: 设置新密码
change_status: 更改状态
change_role: 更改角色
show_logs: 显示日志
add_user: Add user
new_password_modal:
title: Set new password
form:
fields:
password:
label: Password
text: The user will be logged out and need to login again.
msg: Password must be at 8 - 32 characters in length.
btn_cancel: Cancel
btn_submit: Submit
user_modal:
title: Add new user
form:
fields:
display_name:
label: Display Name
msg: display_name must be at 4 - 30 characters in length.
email:
label: Email
msg: Email is not valid.
password:
label: Password
msg: Password must be at 8 - 32 characters in length.
btn_cancel: Cancel
btn_submit: Submit
questions:
page_title: 问题
normal: 正常
@ -876,6 +1117,8 @@ ui:
status: 状态
action: 操作
change: 更改
filter:
placeholder: "Filter by title, question:id"
answers:
page_title: 回答
normal: 正常
@ -886,6 +1129,8 @@ ui:
status: 状态
action: 操作
change: 更改
filter:
placeholder: "Filter by title, answer:id"
general:
page_title: 一般
name:
@ -914,6 +1159,10 @@ ui:
label: 界面语言
msg: 不能为空
text: 设置用户界面语言,在刷新页面后生效。
time_zone:
label: Timezone
msg: Timezone cannot be empty.
text: Choose a city in the same timezone as you.
smtp:
page_title: SMTP
from_email:
@ -949,8 +1198,139 @@ ui:
text: 提供用于接收测试邮件的邮箱地址。
msg: 地址无效
smtp_authentication:
label: SMTP 认证
label: Enable authentication
title: SMTP Authentication
msg: 不能为空
'yes':
'no':
"yes": "是"
"no": "否"
branding:
page_title: Branding
logo:
label: Logo
msg: Logo cannot be empty.
text: The logo image at the top left of your site. Use a wide rectangular image with a height of 56 and an aspect ratio greater than 3:1. If left blank, the site title text will be shown.
mobile_logo:
label: Mobile Logo (optional)
text: The logo used on mobile version of your site. Use a wide rectangular image with a height of 56. If left blank, the image from the “logo” setting will be used.
square_icon:
label: Square Icon
msg: Square icon cannot be empty.
text: Image used as the base for metadata icons. Should ideally be larger than 512x512.
favicon:
label: Favicon (optional)
text: A favicon for your site. To work correctly over a CDN it must be a png. Will be resized to 32x32. If left blank, “square icon” will be used.
legal:
page_title: Legal
terms_of_service:
label: Terms of Service
text: "You can add terms of service content here. If you already have a document hosted elsewhere, provide the full URL here."
privacy_policy:
label: Privacy Policy
text: "You can add privacy policy content here. If you already have a document hosted elsewhere, provide the full URL here."
write:
page_title: Write
recommend_tags:
label: Recommend Tags
text: "Please input tag slug above, one tag per line."
required_tag:
title: Required Tag
label: Set recommend tag as required
text: "Every new question must have at least one recommend tag."
reserved_tags:
label: Reserved Tags
text: "Reserved tags can only be added to a post by moderator."
seo:
page_title: SEO
permalink:
label: Permalink
text: Custom URL structures can improve the usability, and forward-compatibility of your links.
robots:
label: robots.txt
text: This will permanently override any related site settings.
themes:
page_title: Themes
themes:
label: Themes
text: Select an existing theme.
navbar_style:
label: Navbar Style
text: Select an existing theme.
primary_color:
label: Primary Color
text: Modify the colors used by your themes
css_and_html:
page_title: CSS and HTML
custom_css:
label: Custom CSS
text: This will insert as <link>
head:
label: Head
text: This will insert before </head>
header:
label: Header
text: This will insert after <body>
footer:
label: Footer
text: This will insert before </html>.
login:
page_title: Login
membership:
title: Membership
label: Allow new registrations
text: Turn off to prevent anyone from creating a new account.
private:
title: Private
label: Login required
text: Only logged in users can access this community.
form:
empty: cannot be empty
invalid: is invalid
btn_submit: Save
not_found_props: "Required property {{ key }} not found."
page_review:
review: Review
proposed: 提案
question_edit: 问题编辑
answer_edit: 回答编辑
tag_edit: '标签管理: 编辑标签'
edit_summary: 编辑汇总
edit_question: 编辑问题
edit_answer: 编辑回答
edit_tag: 编辑标签
empty: 没有剩余的审核任务。
timeline:
undeleted: 取消删除
deleted: 删除
downvote: 反对
upvote: 赞同
accept: 采纳
cancelled: 已取消
commented: '评论:'
rollback: 回滚
edited: 最后编辑于
answered: 回答于
asked: 提问于
closed: 关闭
reopened: 重新开启
created: 创建于
title: "历史记录"
tag_title: "时间线"
show_votes: "显示投票"
n_or_a: N/A
title_for_question: "时间线"
title_for_answer: "{{ title }} 的 {{ author }} 回答时间线"
title_for_tag: "时间线"
datetime: 日期时间
type: 类型
by:
comment: 评论
no_data: "空空如也"
users:
title: Users
users_with_the_most_reputation: Users with the highest reputation scores
users_with_the_most_vote: Users who voted the most
staffs: Our community staff
reputation: reputation
votes: votes

1336
i18n/zh_TW.yaml Normal file

File diff suppressed because it is too large Load Diff

View File

@ -7,8 +7,8 @@ const (
AnswerTheQuestion = "notification.action.answer_the_question"
// UpdateAnswer update answer
UpdateAnswer = "notification.action.update_answer"
// AdoptAnswer adopt answer
AdoptAnswer = "notification.action.adopt_answer"
// AcceptAnswer accept answer
AcceptAnswer = "notification.action.accept_answer"
// CommentQuestion comment question
CommentQuestion = "notification.action.comment_question"
// CommentAnswer comment answer

View File

@ -52,7 +52,7 @@ func BindAndCheck(ctx *gin.Context, data interface{}) bool {
return true
}
errField, err := validator.GetValidatorByLang(lang.Abbr()).Check(data)
errField, err := validator.GetValidatorByLang(lang).Check(data)
if err != nil {
HandleResponse(ctx, err, errField)
return true
@ -70,6 +70,6 @@ func BindAndCheckReturnErr(ctx *gin.Context, data interface{}) (errFields []*val
return nil
}
errFields, _ = validator.GetValidatorByLang(lang.Abbr()).Check(data)
errFields, _ = validator.GetValidatorByLang(lang).Check(data)
return errFields
}

View File

@ -11,14 +11,10 @@ import (
// GetLang get language from header
func GetLang(ctx *gin.Context) i18n.Language {
acceptLanguage := ctx.GetHeader(constant.AcceptLanguageFlag)
switch i18n.Language(acceptLanguage) {
case i18n.LanguageChinese:
return i18n.LanguageChinese
case i18n.LanguageEnglish:
return i18n.LanguageEnglish
default:
return i18n.DefaultLang
if len(acceptLanguage) == 0 {
return i18n.DefaultLanguage
}
return i18n.Language(acceptLanguage)
}
// GetLangByCtx get language from header
@ -27,5 +23,5 @@ func GetLangByCtx(ctx context.Context) i18n.Language {
if ok {
return acceptLanguage
}
return i18n.DefaultLang
return i18n.DefaultLanguage
}

View File

@ -113,7 +113,7 @@ func (am *AuthUserMiddleware) MustAuth() gin.HandlerFunc {
}
}
func (am *AuthUserMiddleware) CmsAuth() gin.HandlerFunc {
func (am *AuthUserMiddleware) AdminAuth() gin.HandlerFunc {
return func(ctx *gin.Context) {
token := ExtractToken(ctx)
if len(token) == 0 {
@ -121,7 +121,7 @@ func (am *AuthUserMiddleware) CmsAuth() gin.HandlerFunc {
ctx.Abort()
return
}
userInfo, err := am.authService.GetCmsUserCacheInfo(ctx, token)
userInfo, err := am.authService.GetAdminUserCacheInfo(ctx, token)
if err != nil {
handler.HandleResponse(ctx, errors.Unauthorized(reason.UnauthorizedError), nil)
ctx.Abort()

View File

@ -57,9 +57,9 @@ func NewHTTPServer(debug bool,
authV1.Use(authUserMiddleware.MustAuth())
answerRouter.RegisterAnswerAPIRouter(authV1)
cmsauthV1 := r.Group("/answer/admin/api")
cmsauthV1.Use(authUserMiddleware.CmsAuth())
answerRouter.RegisterAnswerCmsAPIRouter(cmsauthV1)
adminauthV1 := r.Group("/answer/admin/api")
adminauthV1.Use(authUserMiddleware.AdminAuth())
answerRouter.RegisterAnswerAdminAPIRouter(adminauthV1)
templateRouter.RegisterTemplateRouter(rootGroup)
return r

View File

@ -49,6 +49,7 @@ var funcMap = template.FuncMap{
k := converter.InterfaceToString(params[i])
v := converter.InterfaceToString(params[i+1])
trans = strings.ReplaceAll(trans, "{{ "+k+" }}", v)
trans = strings.ReplaceAll(trans, "{{"+k+"}}", v)
}
}

View File

@ -2,23 +2,65 @@ package validator
import (
"errors"
"fmt"
"reflect"
"strings"
"github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/internal/base/translator"
"github.com/go-playground/locales"
german "github.com/go-playground/locales/de"
english "github.com/go-playground/locales/en"
zhongwen "github.com/go-playground/locales/zh"
spanish "github.com/go-playground/locales/es"
french "github.com/go-playground/locales/fr"
italian "github.com/go-playground/locales/it"
japanese "github.com/go-playground/locales/ja"
korean "github.com/go-playground/locales/ko"
portuguese "github.com/go-playground/locales/pt"
russian "github.com/go-playground/locales/ru"
vietnamese "github.com/go-playground/locales/vi"
chinese "github.com/go-playground/locales/zh"
chineseTraditional "github.com/go-playground/locales/zh_Hant_TW"
ut "github.com/go-playground/universal-translator"
"github.com/go-playground/validator/v10"
"github.com/go-playground/validator/v10/translations/en"
"github.com/go-playground/validator/v10/translations/es"
"github.com/go-playground/validator/v10/translations/fr"
"github.com/go-playground/validator/v10/translations/it"
"github.com/go-playground/validator/v10/translations/ja"
"github.com/go-playground/validator/v10/translations/pt"
"github.com/go-playground/validator/v10/translations/ru"
"github.com/go-playground/validator/v10/translations/vi"
"github.com/go-playground/validator/v10/translations/zh"
"github.com/go-playground/validator/v10/translations/zh_tw"
myErrors "github.com/segmentfault/pacman/errors"
"github.com/segmentfault/pacman/i18n"
"github.com/segmentfault/pacman/log"
)
type TranslatorLocal struct {
La i18n.Language
Lo locales.Translator
RegisterFunc func(v *validator.Validate, trans ut.Translator) (err error)
}
var (
allLanguageTranslators = []*TranslatorLocal{
{La: i18n.LanguageChinese, Lo: chinese.New(), RegisterFunc: zh.RegisterDefaultTranslations},
{La: i18n.LanguageChineseTraditional, Lo: chineseTraditional.New(), RegisterFunc: zh_tw.RegisterDefaultTranslations},
{La: i18n.LanguageEnglish, Lo: english.New(), RegisterFunc: en.RegisterDefaultTranslations},
{La: i18n.LanguageGerman, Lo: german.New(), RegisterFunc: nil},
{La: i18n.LanguageSpanish, Lo: spanish.New(), RegisterFunc: es.RegisterDefaultTranslations},
{La: i18n.LanguageFrench, Lo: french.New(), RegisterFunc: fr.RegisterDefaultTranslations},
{La: i18n.LanguageItalian, Lo: italian.New(), RegisterFunc: it.RegisterDefaultTranslations},
{La: i18n.LanguageJapanese, Lo: japanese.New(), RegisterFunc: ja.RegisterDefaultTranslations},
{La: i18n.LanguageKorean, Lo: korean.New(), RegisterFunc: nil},
{La: i18n.LanguagePortuguese, Lo: portuguese.New(), RegisterFunc: pt.RegisterDefaultTranslations},
{La: i18n.LanguageRussian, Lo: russian.New(), RegisterFunc: ru.RegisterDefaultTranslations},
{La: i18n.LanguageVietnamese, Lo: vietnamese.New(), RegisterFunc: vi.RegisterDefaultTranslations},
}
)
// MyValidator my validator
type MyValidator struct {
Validate *validator.Validate
@ -33,26 +75,24 @@ type FormErrorField struct {
}
// GlobalValidatorMapping is a mapping from validator to translator used
var GlobalValidatorMapping = make(map[string]*MyValidator, 0)
var GlobalValidatorMapping = make(map[i18n.Language]*MyValidator, 0)
func init() {
zhTran, zhVal := getTran(zhongwen.New(), i18n.LanguageChinese.Abbr()), createDefaultValidator(i18n.LanguageChinese)
if err := zh.RegisterDefaultTranslations(zhVal, zhTran); err != nil {
panic(err)
for _, t := range allLanguageTranslators {
tran, val := getTran(t.Lo), createDefaultValidator(t.La)
if t.RegisterFunc != nil {
if err := t.RegisterFunc(val, tran); err != nil {
panic(err)
}
}
GlobalValidatorMapping[t.La] = &MyValidator{Validate: val, Tran: tran, Lang: t.La}
}
GlobalValidatorMapping[i18n.LanguageChinese.Abbr()] = &MyValidator{Validate: zhVal, Tran: zhTran, Lang: i18n.LanguageChinese}
enTran, enVal := getTran(english.New(), i18n.LanguageEnglish.Abbr()), createDefaultValidator(i18n.LanguageEnglish)
if err := en.RegisterDefaultTranslations(enVal, enTran); err != nil {
panic(err)
}
GlobalValidatorMapping[i18n.LanguageEnglish.Abbr()] = &MyValidator{Validate: enVal, Tran: enTran, Lang: i18n.LanguageEnglish}
}
func getTran(lo locales.Translator, la string) ut.Translator {
tran, ok := ut.New(lo, lo).GetTranslator(la)
func getTran(lo locales.Translator) ut.Translator {
tran, ok := ut.New(lo, lo).GetTranslator(lo.Locale())
if !ok {
panic(ok)
panic(fmt.Sprintf("not found translator %s", lo.Locale()))
}
return tran
}
@ -79,11 +119,11 @@ func createDefaultValidator(la i18n.Language) *validator.Validate {
return validate
}
func GetValidatorByLang(la string) *MyValidator {
if GlobalValidatorMapping[la] != nil {
return GlobalValidatorMapping[la]
func GetValidatorByLang(lang i18n.Language) *MyValidator {
if GlobalValidatorMapping[lang] != nil {
return GlobalValidatorMapping[lang]
}
return GlobalValidatorMapping[i18n.DefaultLang.Abbr()]
return GlobalValidatorMapping[i18n.DefaultLanguage]
}
// Check /

View File

@ -232,18 +232,18 @@ func (ac *AnswerController) AnswerList(ctx *gin.Context) {
})
}
// Adopted godoc
// @Summary Adopted
// @Description Adopted
// Accepted godoc
// @Summary Accepted
// @Description Accepted
// @Tags api-answer
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param data body schema.AnswerAdoptedReq true "AnswerAdoptedReq"
// @Param data body schema.AnswerAcceptedReq true "AnswerAcceptedReq"
// @Success 200 {string} string ""
// @Router /answer/api/v1/answer/acceptance [post]
func (ac *AnswerController) Adopted(ctx *gin.Context) {
req := &schema.AnswerAdoptedReq{}
func (ac *AnswerController) Accepted(ctx *gin.Context) {
req := &schema.AnswerAcceptedReq{}
if handler.BindAndCheck(ctx, req) {
return
}
@ -259,7 +259,7 @@ func (ac *AnswerController) Adopted(ctx *gin.Context) {
return
}
err = ac.answerService.UpdateAdopted(ctx, req)
err = ac.answerService.UpdateAccepted(ctx, req)
handler.HandleResponse(ctx, err, nil)
}

View File

@ -277,6 +277,19 @@ func (qc *QuestionController) AddQuestion(ctx *gin.Context) {
return
}
errList, err := qc.questionService.CheckAddQuestion(ctx, req)
if err != nil {
errlist, ok := errList.([]*validator.FormErrorField)
if ok {
errFields = append(errFields, errlist...)
}
}
if len(errFields) > 0 {
handler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), errFields)
return
}
resp, err := qc.questionService.AddQuestion(ctx, req)
if err != nil {
errlist, ok := resp.([]*validator.FormErrorField)
@ -284,6 +297,7 @@ func (qc *QuestionController) AddQuestion(ctx *gin.Context) {
errFields = append(errFields, errlist...)
}
}
if len(errFields) > 0 {
handler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), errFields)
return
@ -480,8 +494,8 @@ func (qc *QuestionController) UserCollectionList(ctx *gin.Context) {
})
}
// CmsSearchList godoc
// @Summary CmsSearchList
// AdminSearchList godoc
// @Summary AdminSearchList
// @Description Status:[available,closed,deleted]
// @Tags admin
// @Accept json
@ -493,21 +507,21 @@ func (qc *QuestionController) UserCollectionList(ctx *gin.Context) {
// @Param query query string false "question id or title"
// @Success 200 {object} handler.RespBody
// @Router /answer/admin/api/question/page [get]
func (qc *QuestionController) CmsSearchList(ctx *gin.Context) {
req := &schema.CmsQuestionSearch{}
func (qc *QuestionController) AdminSearchList(ctx *gin.Context) {
req := &schema.AdminQuestionSearch{}
if handler.BindAndCheck(ctx, req) {
return
}
userID := middleware.GetLoginUserIDFromContext(ctx)
questionList, count, err := qc.questionService.CmsSearchList(ctx, req, userID)
questionList, count, err := qc.questionService.AdminSearchList(ctx, req, userID)
handler.HandleResponse(ctx, err, gin.H{
"list": questionList,
"count": count,
})
}
// CmsSearchAnswerList godoc
// @Summary CmsSearchList
// AdminSearchAnswerList godoc
// @Summary AdminSearchAnswerList
// @Description Status:[available,deleted]
// @Tags admin
// @Accept json
@ -520,13 +534,13 @@ func (qc *QuestionController) CmsSearchList(ctx *gin.Context) {
// @Param question_id query string false "question id"
// @Success 200 {object} handler.RespBody
// @Router /answer/admin/api/answer/page [get]
func (qc *QuestionController) CmsSearchAnswerList(ctx *gin.Context) {
req := &entity.CmsAnswerSearch{}
func (qc *QuestionController) AdminSearchAnswerList(ctx *gin.Context) {
req := &entity.AdminAnswerSearch{}
if handler.BindAndCheck(ctx, req) {
return
}
userID := middleware.GetLoginUserIDFromContext(ctx)
questionList, count, err := qc.questionService.CmsSearchAnswerList(ctx, req, userID)
questionList, count, err := qc.questionService.AdminSearchAnswerList(ctx, req, userID)
handler.HandleResponse(ctx, err, gin.H{
"list": questionList,
"count": count,

View File

@ -264,7 +264,7 @@ func (tc *TemplateController) QuestionInfo(ctx *gin.Context) {
jsonLD.MainEntity.Author.Name = detail.UserInfo.DisplayName
answerList := make([]*schema.SuggestedAnswerItem, 0)
for _, answer := range answers {
if answer.Adopted == schema.AnswerAdoptedEnable {
if answer.Accepted == schema.AnswerAcceptedEnable {
acceptedAnswerItem := &schema.AcceptedAnswerItem{}
acceptedAnswerItem.Type = "Answer"
acceptedAnswerItem.Text = answer.HTML

View File

@ -1,11 +1,11 @@
package controller_backyard
package controller_admin
import "github.com/google/wire"
// ProviderSetController is controller providers.
var ProviderSetController = wire.NewSet(
NewReportController,
NewUserBackyardController,
NewUserAdminController,
NewThemeController,
NewSiteInfoController,
NewRoleController,

View File

@ -1,20 +1,20 @@
package controller_backyard
package controller_admin
import (
"github.com/answerdev/answer/internal/base/handler"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/report_backyard"
"github.com/answerdev/answer/internal/service/report_admin"
"github.com/answerdev/answer/pkg/converter"
"github.com/gin-gonic/gin"
)
// ReportController report controller
type ReportController struct {
reportService *report_backyard.ReportBackyardService
reportService *report_admin.ReportAdminService
}
// NewReportController new controller
func NewReportController(reportService *report_backyard.ReportBackyardService) *ReportController {
func NewReportController(reportService *report_admin.ReportAdminService) *ReportController {
return &ReportController{reportService: reportService}
}

View File

@ -1,4 +1,4 @@
package controller_backyard
package controller_admin
import (
"github.com/answerdev/answer/internal/base/handler"

View File

@ -1,4 +1,4 @@
package controller_backyard
package controller_admin
import (
"net/http"

View File

@ -1,4 +1,4 @@
package controller_backyard
package controller_admin
import (
"github.com/answerdev/answer/internal/base/handler"

View File

@ -1,21 +1,21 @@
package controller_backyard
package controller_admin
import (
"github.com/answerdev/answer/internal/base/handler"
"github.com/answerdev/answer/internal/base/middleware"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/user_backyard"
"github.com/answerdev/answer/internal/service/user_admin"
"github.com/gin-gonic/gin"
)
// UserBackyardController user controller
type UserBackyardController struct {
userService *user_backyard.UserBackyardService
// UserAdminController user controller
type UserAdminController struct {
userService *user_admin.UserAdminService
}
// NewUserBackyardController new controller
func NewUserBackyardController(userService *user_backyard.UserBackyardService) *UserBackyardController {
return &UserBackyardController{userService: userService}
// NewUserAdminController new controller
func NewUserAdminController(userService *user_admin.UserAdminService) *UserAdminController {
return &UserAdminController{userService: userService}
}
// UpdateUserStatus update user
@ -28,7 +28,7 @@ func NewUserBackyardController(userService *user_backyard.UserBackyardService) *
// @Param data body schema.UpdateUserStatusReq true "user"
// @Success 200 {object} handler.RespBody
// @Router /answer/admin/api/user/status [put]
func (uc *UserBackyardController) UpdateUserStatus(ctx *gin.Context) {
func (uc *UserAdminController) UpdateUserStatus(ctx *gin.Context) {
req := &schema.UpdateUserStatusReq{}
if handler.BindAndCheck(ctx, req) {
return
@ -48,7 +48,7 @@ func (uc *UserBackyardController) UpdateUserStatus(ctx *gin.Context) {
// @Param data body schema.UpdateUserRoleReq true "user"
// @Success 200 {object} handler.RespBody
// @Router /answer/admin/api/user/role [put]
func (uc *UserBackyardController) UpdateUserRole(ctx *gin.Context) {
func (uc *UserAdminController) UpdateUserRole(ctx *gin.Context) {
req := &schema.UpdateUserRoleReq{}
if handler.BindAndCheck(ctx, req) {
return
@ -70,7 +70,7 @@ func (uc *UserBackyardController) UpdateUserRole(ctx *gin.Context) {
// @Param data body schema.AddUserReq true "user"
// @Success 200 {object} handler.RespBody
// @Router /answer/admin/api/user [post]
func (uc *UserBackyardController) AddUser(ctx *gin.Context) {
func (uc *UserAdminController) AddUser(ctx *gin.Context) {
req := &schema.AddUserReq{}
if handler.BindAndCheck(ctx, req) {
return
@ -92,7 +92,7 @@ func (uc *UserBackyardController) AddUser(ctx *gin.Context) {
// @Param data body schema.UpdateUserPasswordReq true "user"
// @Success 200 {object} handler.RespBody
// @Router /answer/admin/api/user/password [put]
func (uc *UserBackyardController) UpdateUserPassword(ctx *gin.Context) {
func (uc *UserAdminController) UpdateUserPassword(ctx *gin.Context) {
req := &schema.UpdateUserPasswordReq{}
if handler.BindAndCheck(ctx, req) {
return
@ -117,7 +117,7 @@ func (uc *UserBackyardController) UpdateUserPassword(ctx *gin.Context) {
// @Param status query string false "user status" Enums(suspended, deleted, inactive)
// @Success 200 {object} handler.RespBody{data=pager.PageModel{records=[]schema.GetUserPageResp}}
// @Router /answer/admin/api/users/page [get]
func (uc *UserBackyardController) GetUserPage(ctx *gin.Context) {
func (uc *UserAdminController) GetUserPage(ctx *gin.Context) {
req := &schema.GetUserPageReq{}
if handler.BindAndCheck(ctx, req) {
return

View File

@ -11,7 +11,7 @@ const (
AnswerStatusDeleted = 10
)
var CmsAnswerSearchStatus = map[string]int{
var AdminAnswerSearchStatus = map[string]int{
"available": AnswerStatusAvailable,
"deleted": AnswerStatusDeleted,
}
@ -27,7 +27,7 @@ type Answer struct {
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"`
Adopted int `xorm:"not null default 1 INT(11) adopted"`
Accepted int `xorm:"not null default 1 INT(11) adopted"`
CommentCount int `xorm:"not null default 0 INT(11) comment_count"`
VoteCount int `xorm:"not null default 0 INT(11) vote_count"`
RevisionID string `xorm:"not null default 0 BIGINT(20) revision_id"`
@ -40,7 +40,7 @@ type AnswerSearch struct {
PageSize int `json:"page_size" form:"page_size"` // Search page size
}
type CmsAnswerSearch struct {
type AdminAnswerSearch struct {
Page int `json:"page" form:"page"` // Query number of pages
PageSize int `json:"page_size" form:"page_size"` // Search page size
Status int `json:"-" form:"-"`

View File

@ -10,13 +10,13 @@ const (
QuestionStatusDeleted = 10
)
var CmsQuestionSearchStatus = map[string]int{
var AdminQuestionSearchStatus = map[string]int{
"available": QuestionStatusAvailable,
"closed": QuestionStatusClosed,
"deleted": QuestionStatusDeleted,
}
var CmsQuestionSearchStatusIntToString = map[int]string{
var AdminQuestionSearchStatusIntToString = map[int]string{
QuestionStatusAvailable: "available",
QuestionStatusClosed: "closed",
QuestionStatusDeleted: "deleted",

View File

@ -1,6 +1,7 @@
package migrations
import (
"context"
"fmt"
"github.com/answerdev/answer/internal/base/data"
@ -14,11 +15,13 @@ const minDBVersion = 0 // answer 1.0.0
type Migration interface {
Description() string
Migrate(*xorm.Engine) error
ShouldCleanCache() bool
}
type migration struct {
description string
migrate func(*xorm.Engine) error
description string
migrate func(*xorm.Engine) error
shouldCleanCache bool
}
// Description returns the migration's description
@ -31,9 +34,14 @@ func (m *migration) Migrate(x *xorm.Engine) error {
return m.migrate(x)
}
// ShouldCleanCache should clean the cache
func (m *migration) ShouldCleanCache() bool {
return m.shouldCleanCache
}
// NewMigration creates a new migration
func NewMigration(desc string, fn func(*xorm.Engine) error) Migration {
return &migration{description: desc, migrate: fn}
func NewMigration(desc string, fn func(*xorm.Engine) error, shouldCleanCache bool) Migration {
return &migration{description: desc, migrate: fn, shouldCleanCache: shouldCleanCache}
}
// Use noopMigration when there is a migration that has been no-oped
@ -41,12 +49,12 @@ var noopMigration = func(_ *xorm.Engine) error { return nil }
var migrations = []Migration{
// 0->1
NewMigration("this is first version, no operation", noopMigration),
NewMigration("add user language", addUserLanguage),
NewMigration("add recommend and reserved tag fields", addTagRecommendedAndReserved),
NewMigration("add activity timeline", addActivityTimeline),
NewMigration("add user role", addRoleFeatures),
NewMigration("add theme and private mode", addThemeAndPrivateMode),
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),
}
// GetCurrentDBVersion returns the current db version
@ -76,8 +84,12 @@ func ExpectedVersion() int64 {
}
// Migrate database to current version
func Migrate(dataConf *data.Database) error {
engine, err := data.NewDB(false, dataConf)
func Migrate(dbConf *data.Database, cacheConf *data.CacheConf) error {
cache, cacheCleanup, err := data.NewCache(cacheConf)
if err != nil {
fmt.Println("new check failed:", err.Error())
}
engine, err := data.NewDB(false, dbConf)
if err != nil {
fmt.Println("new database failed: ", err.Error())
return err
@ -98,6 +110,11 @@ func Migrate(dataConf *data.Database) error {
fmt.Printf("[migrate] migrate to db version %d failed: %s\n", currentDBVersion+1, err.Error())
return err
}
if migrationFunc.ShouldCleanCache() {
if err := cache.Flush(context.Background()); err != nil {
fmt.Printf("[migrate] flush cache failed: %s\n", err.Error())
}
}
fmt.Printf("[migrate] migrate to db version %d success\n", currentDBVersion+1)
if _, err := engine.Update(&entity.Version{ID: 1, VersionNumber: currentDBVersion + 1}); err != nil {
fmt.Printf("[migrate] migrate to db version %d, update failed: %s", currentDBVersion+1, err.Error())
@ -105,5 +122,8 @@ func Migrate(dataConf *data.Database) error {
}
currentDBVersion++
}
if cache != nil {
cacheCleanup()
}
return nil
}

View File

@ -214,7 +214,7 @@ func (ar *AnswerActivityRepo) AcceptAnswer(ctx context.Context,
if act.UserID != questionUserID {
msg.TriggerUserID = questionUserID
msg.ObjectType = constant.AnswerObjectType
msg.NotificationAction = constant.AdoptAnswer
msg.NotificationAction = constant.AcceptAnswer
notice_queue.AddNotification(msg)
}
}

View File

@ -220,7 +220,7 @@ func (vr *VoteRepo) voteCancel(ctx context.Context, objectID string, userID, obj
}
// trigger user rank and send notification
if hasRank != 0 && existsActivity.Rank > 0 {
if hasRank != 0 && existsActivity.Rank != 0 {
_, err = vr.userRankRepo.TriggerUserRank(ctx, session, activityUserID, -deltaRank, activityType)
if err != nil {
return

View File

@ -133,22 +133,22 @@ func (ar *answerRepo) GetAnswerPage(ctx context.Context, page, pageSize int, ans
return
}
// UpdateAdopted
// UpdateAccepted
// If no answer is selected, the answer id can be 0
func (ar *answerRepo) UpdateAdopted(ctx context.Context, id string, questionID string) error {
func (ar *answerRepo) UpdateAccepted(ctx context.Context, id string, questionID string) error {
if questionID == "" {
return nil
}
var data entity.Answer
data.ID = id
data.Adopted = schema.AnswerAdoptedFailed
data.Accepted = schema.AnswerAcceptedFailed
_, err := ar.data.DB.Where("question_id =?", questionID).Cols("adopted").Update(&data)
if err != nil {
return err
}
if id != "0" {
data.Adopted = schema.AnswerAdoptedEnable
data.Accepted = schema.AnswerAcceptedEnable
_, err = ar.data.DB.Where("id = ?", id).Cols("adopted").Update(&data)
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
@ -216,7 +216,7 @@ func (ar *answerRepo) SearchList(ctx context.Context, search *entity.AnswerSearc
return rows, count, nil
}
func (ar *answerRepo) CmsSearchList(ctx context.Context, search *entity.CmsAnswerSearch) ([]*entity.Answer, int64, error) {
func (ar *answerRepo) AdminSearchList(ctx context.Context, search *entity.AdminAnswerSearch) ([]*entity.Answer, int64, error) {
var (
count int64
err error

View File

@ -95,8 +95,8 @@ func (ar *authRepo) RemoveUserStatus(ctx context.Context, userID string) (err er
return nil
}
// GetBackyardUserCacheInfo get backyard user cache info
func (ar *authRepo) GetBackyardUserCacheInfo(ctx context.Context, accessToken string) (userInfo *entity.UserCacheInfo, err error) {
// GetAdminUserCacheInfo get admin user cache info
func (ar *authRepo) GetAdminUserCacheInfo(ctx context.Context, accessToken string) (userInfo *entity.UserCacheInfo, err error) {
userInfoCache, err := ar.data.Cache.GetString(ctx, constant.AdminTokenCacheKey+accessToken)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
@ -110,8 +110,8 @@ func (ar *authRepo) GetBackyardUserCacheInfo(ctx context.Context, accessToken st
return userInfo, nil
}
// SetBackyardUserCacheInfo set backyard user cache info
func (ar *authRepo) SetBackyardUserCacheInfo(ctx context.Context, accessToken string, userInfo *entity.UserCacheInfo) (err error) {
// SetAdminUserCacheInfo set admin user cache info
func (ar *authRepo) SetAdminUserCacheInfo(ctx context.Context, accessToken string, userInfo *entity.UserCacheInfo) (err error) {
userInfoCache, err := json.Marshal(userInfo)
if err != nil {
return err
@ -125,8 +125,8 @@ func (ar *authRepo) SetBackyardUserCacheInfo(ctx context.Context, accessToken st
return nil
}
// RemoveBackyardUserCacheInfo remove backyard user cache info
func (ar *authRepo) RemoveBackyardUserCacheInfo(ctx context.Context, accessToken string) (err error) {
// RemoveAdminUserCacheInfo remove admin user cache info
func (ar *authRepo) RemoveAdminUserCacheInfo(ctx context.Context, accessToken string) (err error) {
err = ar.data.Cache.Del(ctx, constant.AdminTokenCacheKey+accessToken)
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()

View File

@ -25,7 +25,7 @@ func NewCaptchaRepo(data *data.Data) action.CaptchaRepo {
}
func (cr *captchaRepo) SetActionType(ctx context.Context, ip, actionType string, amount int) (err error) {
cacheKey := fmt.Sprintf("ActionRecord:%s@%s", ip, actionType)
cacheKey := fmt.Sprintf("ActionRecord:%s@", ip)
err = cr.data.Cache.SetInt64(ctx, cacheKey, int64(amount), 6*time.Minute)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
@ -34,17 +34,16 @@ func (cr *captchaRepo) SetActionType(ctx context.Context, ip, actionType string,
}
func (cr *captchaRepo) GetActionType(ctx context.Context, ip, actionType string) (amount int, err error) {
cacheKey := fmt.Sprintf("ActionRecord:%s@%s", ip, actionType)
cacheKey := fmt.Sprintf("ActionRecord:%s@", ip)
res, err := cr.data.Cache.GetInt64(ctx, cacheKey)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
// TODO: cache reflect should return empty when key not found
return int(res), nil
}
func (cr *captchaRepo) DelActionType(ctx context.Context, ip, actionType string) (err error) {
cacheKey := fmt.Sprintf("ActionRecord:%s@%s", ip, actionType)
cacheKey := fmt.Sprintf("ActionRecord:%s@", ip)
err = cr.data.Cache.Del(ctx, cacheKey)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
@ -54,7 +53,6 @@ func (cr *captchaRepo) DelActionType(ctx context.Context, ip, actionType string)
// SetCaptcha set captcha to cache
func (cr *captchaRepo) SetCaptcha(ctx context.Context, key, captcha string) (err error) {
// TODO make cache time to config
err = cr.data.Cache.SetString(ctx, key, captcha, 6*time.Minute)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
@ -68,6 +66,5 @@ func (cr *captchaRepo) GetCaptcha(ctx context.Context, key string) (captcha stri
if err != nil {
log.Debug(err)
}
// TODO: cache reflect should return empty when key not found
return captcha, nil
}

View File

@ -44,7 +44,7 @@ var ProviderSetRepo = wire.NewSet(
activity_common.NewVoteRepo,
config.NewConfigRepo,
user.NewUserRepo,
user.NewUserBackyardRepo,
user.NewUserAdminRepo,
rank.NewUserRankRepo,
question.NewQuestionRepo,
answer.NewAnswerRepo,

View File

@ -271,7 +271,7 @@ func (qr *questionRepo) SearchList(ctx context.Context, search *schema.QuestionS
return rows, count, nil
}
func (qr *questionRepo) CmsSearchList(ctx context.Context, search *schema.CmsQuestionSearch) ([]*entity.Question, int64, error) {
func (qr *questionRepo) AdminSearchList(ctx context.Context, search *schema.AdminQuestionSearch) ([]*entity.Question, int64, error) {
var (
count int64
err error

View File

@ -61,26 +61,26 @@ func Test_authRepo_RemoveUserStatus(t *testing.T) {
assert.Error(t, err)
}
func Test_authRepo_SetBackyardUserCacheInfo(t *testing.T) {
func Test_authRepo_SetAdminUserCacheInfo(t *testing.T) {
authRepo := auth.NewAuthRepo(testDataSource)
err := authRepo.SetBackyardUserCacheInfo(context.TODO(), token, &entity.UserCacheInfo{UserID: userID})
err := authRepo.SetAdminUserCacheInfo(context.TODO(), token, &entity.UserCacheInfo{UserID: userID})
assert.NoError(t, err)
cacheInfo, err := authRepo.GetBackyardUserCacheInfo(context.TODO(), token)
cacheInfo, err := authRepo.GetAdminUserCacheInfo(context.TODO(), token)
assert.NoError(t, err)
assert.Equal(t, userID, cacheInfo.UserID)
}
func Test_authRepo_RemoveBackyardUserCacheInfo(t *testing.T) {
func Test_authRepo_RemoveAdminUserCacheInfo(t *testing.T) {
authRepo := auth.NewAuthRepo(testDataSource)
err := authRepo.SetBackyardUserCacheInfo(context.TODO(), token, &entity.UserCacheInfo{UserID: userID})
err := authRepo.SetAdminUserCacheInfo(context.TODO(), token, &entity.UserCacheInfo{UserID: userID})
assert.NoError(t, err)
err = authRepo.RemoveBackyardUserCacheInfo(context.TODO(), token)
err = authRepo.RemoveAdminUserCacheInfo(context.TODO(), token)
assert.NoError(t, err)
_, err = authRepo.GetBackyardUserCacheInfo(context.TODO(), token)
_, err = authRepo.GetAdminUserCacheInfo(context.TODO(), token)
assert.Error(t, err)
}

View File

@ -10,43 +10,43 @@ import (
"github.com/stretchr/testify/assert"
)
func Test_userBackyardRepo_GetUserInfo(t *testing.T) {
userBackyardRepo := user.NewUserBackyardRepo(testDataSource, auth.NewAuthRepo(testDataSource))
got, exist, err := userBackyardRepo.GetUserInfo(context.TODO(), "1")
func Test_userAdminRepo_GetUserInfo(t *testing.T) {
userAdminRepo := user.NewUserAdminRepo(testDataSource, auth.NewAuthRepo(testDataSource))
got, exist, err := userAdminRepo.GetUserInfo(context.TODO(), "1")
assert.NoError(t, err)
assert.True(t, exist)
assert.Equal(t, "1", got.ID)
}
func Test_userBackyardRepo_GetUserPage(t *testing.T) {
userBackyardRepo := user.NewUserBackyardRepo(testDataSource, auth.NewAuthRepo(testDataSource))
got, total, err := userBackyardRepo.GetUserPage(context.TODO(), 1, 1, &entity.User{Username: "admin"}, "", false)
func Test_userAdminRepo_GetUserPage(t *testing.T) {
userAdminRepo := user.NewUserAdminRepo(testDataSource, auth.NewAuthRepo(testDataSource))
got, total, err := userAdminRepo.GetUserPage(context.TODO(), 1, 1, &entity.User{Username: "admin"}, "", false)
assert.NoError(t, err)
assert.Equal(t, int64(1), total)
assert.Equal(t, "1", got[0].ID)
}
func Test_userBackyardRepo_UpdateUserStatus(t *testing.T) {
userBackyardRepo := user.NewUserBackyardRepo(testDataSource, auth.NewAuthRepo(testDataSource))
got, exist, err := userBackyardRepo.GetUserInfo(context.TODO(), "1")
func Test_userAdminRepo_UpdateUserStatus(t *testing.T) {
userAdminRepo := user.NewUserAdminRepo(testDataSource, auth.NewAuthRepo(testDataSource))
got, exist, err := userAdminRepo.GetUserInfo(context.TODO(), "1")
assert.NoError(t, err)
assert.True(t, exist)
assert.Equal(t, entity.UserStatusAvailable, got.Status)
err = userBackyardRepo.UpdateUserStatus(context.TODO(), "1", entity.UserStatusSuspended, entity.EmailStatusAvailable,
err = userAdminRepo.UpdateUserStatus(context.TODO(), "1", entity.UserStatusSuspended, entity.EmailStatusAvailable,
"admin@admin.com")
assert.NoError(t, err)
got, exist, err = userBackyardRepo.GetUserInfo(context.TODO(), "1")
got, exist, err = userAdminRepo.GetUserInfo(context.TODO(), "1")
assert.NoError(t, err)
assert.True(t, exist)
assert.Equal(t, entity.UserStatusSuspended, got.Status)
err = userBackyardRepo.UpdateUserStatus(context.TODO(), "1", entity.UserStatusAvailable, entity.EmailStatusAvailable,
err = userAdminRepo.UpdateUserStatus(context.TODO(), "1", entity.UserStatusAvailable, entity.EmailStatusAvailable,
"admin@admin.com")
assert.NoError(t, err)
got, exist, err = userBackyardRepo.GetUserInfo(context.TODO(), "1")
got, exist, err = userAdminRepo.GetUserInfo(context.TODO(), "1")
assert.NoError(t, err)
assert.True(t, exist)
assert.Equal(t, entity.UserStatusAvailable, got.Status)

View File

@ -371,8 +371,8 @@ func (sr *searchRepo) SearchAnswers(ctx context.Context, words []string, tagIDs
// check limit accepted
if accepted {
b.Where(builder.Eq{"adopted": schema.AnswerAdoptedEnable})
args = append(args, schema.AnswerAdoptedEnable)
b.Where(builder.Eq{"adopted": schema.AnswerAcceptedEnable})
args = append(args, schema.AnswerAcceptedEnable)
}
// check question id
@ -475,14 +475,14 @@ func (sr *searchRepo) parseResult(ctx context.Context, res []map[string][]byte)
_ = copier.Copy(&tags, tagsEntity)
switch objectKey {
case "question":
for k, v := range entity.CmsQuestionSearchStatus {
for k, v := range entity.AdminQuestionSearchStatus {
if v == converter.StringToInt(string(r["status"])) {
status = k
break
}
}
case "answer":
for k, v := range entity.CmsAnswerSearchStatus {
for k, v := range entity.AdminAnswerSearchStatus {
if v == converter.StringToInt(string(r["status"])) {
status = k
break

View File

@ -12,27 +12,27 @@ import (
"github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/internal/entity"
"github.com/answerdev/answer/internal/service/auth"
"github.com/answerdev/answer/internal/service/user_backyard"
"github.com/answerdev/answer/internal/service/user_admin"
"github.com/segmentfault/pacman/errors"
"github.com/segmentfault/pacman/log"
)
// userBackyardRepo user repository
type userBackyardRepo struct {
// userAdminRepo user repository
type userAdminRepo struct {
data *data.Data
authRepo auth.AuthRepo
}
// NewUserBackyardRepo new repository
func NewUserBackyardRepo(data *data.Data, authRepo auth.AuthRepo) user_backyard.UserBackyardRepo {
return &userBackyardRepo{
// NewUserAdminRepo new repository
func NewUserAdminRepo(data *data.Data, authRepo auth.AuthRepo) user_admin.UserAdminRepo {
return &userAdminRepo{
data: data,
authRepo: authRepo,
}
}
// UpdateUserStatus update user status
func (ur *userBackyardRepo) UpdateUserStatus(ctx context.Context, userID string, userStatus, mailStatus int,
func (ur *userAdminRepo) UpdateUserStatus(ctx context.Context, userID string, userStatus, mailStatus int,
email string,
) (err error) {
cond := &entity.User{Status: userStatus, MailStatus: mailStatus, EMail: email}
@ -62,7 +62,7 @@ func (ur *userBackyardRepo) UpdateUserStatus(ctx context.Context, userID string,
}
// AddUser add user
func (ur *userBackyardRepo) AddUser(ctx context.Context, user *entity.User) (err error) {
func (ur *userAdminRepo) AddUser(ctx context.Context, user *entity.User) (err error) {
_, err = ur.data.DB.Insert(user)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
@ -71,7 +71,7 @@ func (ur *userBackyardRepo) AddUser(ctx context.Context, user *entity.User) (err
}
// UpdateUserPassword update user password
func (ur *userBackyardRepo) UpdateUserPassword(ctx context.Context, userID string, password string) (err error) {
func (ur *userAdminRepo) UpdateUserPassword(ctx context.Context, userID string, password string) (err error) {
_, err = ur.data.DB.ID(userID).Update(&entity.User{Pass: password})
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
@ -80,7 +80,7 @@ func (ur *userBackyardRepo) UpdateUserPassword(ctx context.Context, userID strin
}
// GetUserInfo get user info
func (ur *userBackyardRepo) GetUserInfo(ctx context.Context, userID string) (user *entity.User, exist bool, err error) {
func (ur *userAdminRepo) GetUserInfo(ctx context.Context, userID string) (user *entity.User, exist bool, err error) {
user = &entity.User{}
exist, err = ur.data.DB.ID(userID).Get(user)
if err != nil {
@ -90,7 +90,7 @@ func (ur *userBackyardRepo) GetUserInfo(ctx context.Context, userID string) (use
}
// GetUserInfoByEmail get user info
func (ur *userBackyardRepo) GetUserInfoByEmail(ctx context.Context, email string) (user *entity.User, exist bool, err error) {
func (ur *userAdminRepo) GetUserInfoByEmail(ctx context.Context, email string) (user *entity.User, exist bool, err error) {
userInfo := &entity.User{}
exist, err = ur.data.DB.Where("e_mail = ?", email).
Where("status != ?", entity.UserStatusDeleted).Get(userInfo)
@ -101,7 +101,7 @@ func (ur *userBackyardRepo) GetUserInfoByEmail(ctx context.Context, email string
}
// GetUserPage get user page
func (ur *userBackyardRepo) GetUserPage(ctx context.Context, page, pageSize int, user *entity.User,
func (ur *userAdminRepo) GetUserPage(ctx context.Context, page, pageSize int, user *entity.User,
usernameOrDisplayName string, isStaff bool) (users []*entity.User, total int64, err error) {
users = make([]*entity.User, 0)
session := ur.data.DB.NewSession()

View File

@ -2,35 +2,35 @@ package router
import (
"github.com/answerdev/answer/internal/controller"
"github.com/answerdev/answer/internal/controller_backyard"
"github.com/answerdev/answer/internal/controller_admin"
"github.com/gin-gonic/gin"
)
type AnswerAPIRouter struct {
langController *controller.LangController
userController *controller.UserController
commentController *controller.CommentController
reportController *controller.ReportController
voteController *controller.VoteController
tagController *controller.TagController
followController *controller.FollowController
collectionController *controller.CollectionController
questionController *controller.QuestionController
answerController *controller.AnswerController
searchController *controller.SearchController
revisionController *controller.RevisionController
rankController *controller.RankController
backyardReportController *controller_backyard.ReportController
backyardUserController *controller_backyard.UserBackyardController
reasonController *controller.ReasonController
themeController *controller_backyard.ThemeController
siteInfoController *controller_backyard.SiteInfoController
siteinfoController *controller.SiteinfoController
notificationController *controller.NotificationController
dashboardController *controller.DashboardController
uploadController *controller.UploadController
activityController *controller.ActivityController
roleController *controller_backyard.RoleController
langController *controller.LangController
userController *controller.UserController
commentController *controller.CommentController
reportController *controller.ReportController
voteController *controller.VoteController
tagController *controller.TagController
followController *controller.FollowController
collectionController *controller.CollectionController
questionController *controller.QuestionController
answerController *controller.AnswerController
searchController *controller.SearchController
revisionController *controller.RevisionController
rankController *controller.RankController
adminReportController *controller_admin.ReportController
adminUserController *controller_admin.UserAdminController
reasonController *controller.ReasonController
themeController *controller_admin.ThemeController
siteInfoController *controller_admin.SiteInfoController
siteinfoController *controller.SiteinfoController
notificationController *controller.NotificationController
dashboardController *controller.DashboardController
uploadController *controller.UploadController
activityController *controller.ActivityController
roleController *controller_admin.RoleController
}
func NewAnswerAPIRouter(
@ -47,43 +47,43 @@ func NewAnswerAPIRouter(
searchController *controller.SearchController,
revisionController *controller.RevisionController,
rankController *controller.RankController,
backyardReportController *controller_backyard.ReportController,
backyardUserController *controller_backyard.UserBackyardController,
adminReportController *controller_admin.ReportController,
adminUserController *controller_admin.UserAdminController,
reasonController *controller.ReasonController,
themeController *controller_backyard.ThemeController,
siteInfoController *controller_backyard.SiteInfoController,
themeController *controller_admin.ThemeController,
siteInfoController *controller_admin.SiteInfoController,
siteinfoController *controller.SiteinfoController,
notificationController *controller.NotificationController,
dashboardController *controller.DashboardController,
uploadController *controller.UploadController,
activityController *controller.ActivityController,
roleController *controller_backyard.RoleController,
roleController *controller_admin.RoleController,
) *AnswerAPIRouter {
return &AnswerAPIRouter{
langController: langController,
userController: userController,
commentController: commentController,
reportController: reportController,
voteController: voteController,
tagController: tagController,
followController: followController,
collectionController: collectionController,
questionController: questionController,
answerController: answerController,
searchController: searchController,
revisionController: revisionController,
rankController: rankController,
backyardReportController: backyardReportController,
backyardUserController: backyardUserController,
reasonController: reasonController,
themeController: themeController,
siteInfoController: siteInfoController,
notificationController: notificationController,
siteinfoController: siteinfoController,
dashboardController: dashboardController,
uploadController: uploadController,
activityController: activityController,
roleController: roleController,
langController: langController,
userController: userController,
commentController: commentController,
reportController: reportController,
voteController: voteController,
tagController: tagController,
followController: followController,
collectionController: collectionController,
questionController: questionController,
answerController: answerController,
searchController: searchController,
revisionController: revisionController,
rankController: rankController,
adminReportController: adminReportController,
adminUserController: adminUserController,
reasonController: reasonController,
themeController: themeController,
siteInfoController: siteInfoController,
notificationController: notificationController,
siteinfoController: siteinfoController,
dashboardController: dashboardController,
uploadController: uploadController,
activityController: activityController,
roleController: roleController,
}
}
@ -194,7 +194,7 @@ func (a *AnswerAPIRouter) RegisterAnswerAPIRouter(r *gin.RouterGroup) {
// answer
r.POST("/answer", a.answerController.Add)
r.PUT("/answer", a.answerController.Update)
r.POST("/answer/acceptance", a.answerController.Adopted)
r.POST("/answer/acceptance", a.answerController.Accepted)
r.DELETE("/answer", a.answerController.RemoveAnswer)
// user
@ -225,22 +225,22 @@ func (a *AnswerAPIRouter) RegisterAnswerAPIRouter(r *gin.RouterGroup) {
}
func (a *AnswerAPIRouter) RegisterAnswerCmsAPIRouter(r *gin.RouterGroup) {
r.GET("/question/page", a.questionController.CmsSearchList)
func (a *AnswerAPIRouter) RegisterAnswerAdminAPIRouter(r *gin.RouterGroup) {
r.GET("/question/page", a.questionController.AdminSearchList)
r.PUT("/question/status", a.questionController.AdminSetQuestionStatus)
r.GET("/answer/page", a.questionController.CmsSearchAnswerList)
r.GET("/answer/page", a.questionController.AdminSearchAnswerList)
r.PUT("/answer/status", a.answerController.AdminSetAnswerStatus)
// report
r.GET("/reports/page", a.backyardReportController.ListReportPage)
r.PUT("/report", a.backyardReportController.Handle)
r.GET("/reports/page", a.adminReportController.ListReportPage)
r.PUT("/report", a.adminReportController.Handle)
// user
r.GET("/users/page", a.backyardUserController.GetUserPage)
r.PUT("/user/status", a.backyardUserController.UpdateUserStatus)
r.PUT("/user/role", a.backyardUserController.UpdateUserRole)
r.POST("/user", a.backyardUserController.AddUser)
r.PUT("/user/password", a.backyardUserController.UpdateUserPassword)
r.GET("/users/page", a.adminUserController.GetUserPage)
r.PUT("/user/status", a.adminUserController.UpdateUserStatus)
r.PUT("/user/role", a.adminUserController.UpdateUserRole)
r.POST("/user", a.adminUserController.AddUser)
r.PUT("/user/password", a.adminUserController.UpdateUserPassword)
// reason
r.GET("/reasons", a.reasonController.Reasons)

View File

@ -3,20 +3,20 @@ package router
import (
"github.com/answerdev/answer/internal/controller"
templaterender "github.com/answerdev/answer/internal/controller/template_render"
"github.com/answerdev/answer/internal/controller_backyard"
"github.com/answerdev/answer/internal/controller_admin"
"github.com/gin-gonic/gin"
)
type TemplateRouter struct {
templateController *controller.TemplateController
templateRenderController *templaterender.TemplateRenderController
siteInfoController *controller_backyard.SiteInfoController
siteInfoController *controller_admin.SiteInfoController
}
func NewTemplateRouter(
templateController *controller.TemplateController,
templateRenderController *templaterender.TemplateRenderController,
siteInfoController *controller_backyard.SiteInfoController,
siteInfoController *controller_admin.SiteInfoController,
) *TemplateRouter {
return &TemplateRouter{

View File

@ -15,8 +15,8 @@ type RemoveAnswerReq struct {
}
const (
AnswerAdoptedFailed = 1
AnswerAdoptedEnable = 2
AnswerAcceptedFailed = 1
AnswerAcceptedEnable = 2
)
type AnswerAddReq struct {
@ -74,7 +74,7 @@ type AnswerInfo struct {
HTML string `json:"html" xorm:"html"` // html
CreateTime int64 `json:"create_time" xorm:"created"` // create_time
UpdateTime int64 `json:"update_time" xorm:"updated"` // update_time
Adopted int `json:"adopted"` // 1 Failed 2 Adopted
Accepted int `json:"accepted"` // 1 Failed 2 accepted
UserID string `json:"-" `
UpdateUserID string `json:"-" `
UserInfo *UserBasicInfo `json:"user_info,omitempty"`
@ -94,7 +94,7 @@ type AdminAnswerInfo struct {
Description string `json:"description"`
CreateTime int64 `json:"create_time"`
UpdateTime int64 `json:"update_time"`
Adopted int `json:"adopted"`
Accepted int `json:"accepted"`
UserID string `json:"-" `
UpdateUserID string `json:"-" `
UserInfo *UserBasicInfo `json:"user_info"`
@ -104,7 +104,7 @@ type AdminAnswerInfo struct {
} `json:"question_info"`
}
type AnswerAdoptedReq struct {
type AnswerAcceptedReq struct {
QuestionID string `json:"question_id"`
AnswerID string `json:"answer_id"`
UserID string `json:"-" `

View File

@ -199,7 +199,7 @@ type GetCloseTypeResp struct {
type UserAnswerInfo struct {
AnswerID string `json:"answer_id"`
QuestionID string `json:"question_id"`
Adopted int `json:"adopted"`
Accepted int `json:"accepted"`
VoteCount int `json:"vote_count"`
CreateTime int `json:"create_time"`
UpdateTime int `json:"update_time"`
@ -233,7 +233,7 @@ type QuestionSearch struct {
UserID string `json:"-" form:"-"`
}
type CmsQuestionSearch struct {
type AdminQuestionSearch struct {
Page int `json:"page" form:"page"` // Query number of pages
PageSize int `json:"page_size" form:"page_size"` // Search page size
Status int `json:"-" form:"-"`

View File

@ -36,7 +36,6 @@ func (r *SiteGeneralReq) FormatSiteUrl() {
// SiteInterfaceReq site interface request
type SiteInterfaceReq struct {
Theme string `validate:"required,gt=1,lte=128" form:"theme" json:"theme"`
Language string `validate:"required,gt=1,lte=128" form:"language" json:"language"`
TimeZone string `validate:"required,gt=1,lte=128" form:"time_zone" json:"time_zone"`
}

View File

@ -15,11 +15,11 @@ type AnswerRepo interface {
GetAnswer(ctx context.Context, id string) (answer *entity.Answer, exist bool, err error)
GetAnswerList(ctx context.Context, answer *entity.Answer) (answerList []*entity.Answer, err error)
GetAnswerPage(ctx context.Context, page, pageSize int, answer *entity.Answer) (answerList []*entity.Answer, total int64, err error)
UpdateAdopted(ctx context.Context, id string, questionID string) error
UpdateAccepted(ctx context.Context, id string, questionID string) error
GetByID(ctx context.Context, id string) (*entity.Answer, bool, error)
GetByUserIDQuestionID(ctx context.Context, userID string, questionID string) (*entity.Answer, bool, error)
SearchList(ctx context.Context, search *entity.AnswerSearch) ([]*entity.Answer, int64, error)
CmsSearchList(ctx context.Context, search *entity.CmsAnswerSearch) ([]*entity.Answer, int64, error)
AdminSearchList(ctx context.Context, search *entity.AdminAnswerSearch) ([]*entity.Answer, int64, error)
UpdateAnswerStatus(ctx context.Context, answer *entity.Answer) (err error)
GetAnswerCount(ctx context.Context) (count int64, err error)
}
@ -43,11 +43,11 @@ func (as *AnswerCommon) SearchAnswered(ctx context.Context, userID, questionID s
return has, nil
}
func (as *AnswerCommon) CmsSearchList(ctx context.Context, search *entity.CmsAnswerSearch) ([]*entity.Answer, int64, error) {
func (as *AnswerCommon) AdminSearchList(ctx context.Context, search *entity.AdminAnswerSearch) ([]*entity.Answer, int64, error) {
if search.Status == 0 {
search.Status = 1
}
return as.answerRepo.CmsSearchList(ctx, search)
return as.answerRepo.AdminSearchList(ctx, search)
}
func (as *AnswerCommon) Search(ctx context.Context, search *entity.AnswerSearch) ([]*entity.Answer, int64, error) {
@ -64,7 +64,7 @@ func (as *AnswerCommon) ShowFormat(ctx context.Context, data *entity.Answer) *sc
info.QuestionID = data.QuestionID
info.Content = data.OriginalText
info.HTML = data.ParsedText
info.Adopted = data.Adopted
info.Accepted = data.Accepted
info.VoteCount = data.VoteCount
info.CreateTime = data.CreatedAt.Unix()
info.UpdateTime = data.UpdatedAt.Unix()
@ -80,7 +80,7 @@ func (as *AnswerCommon) AdminShowFormat(ctx context.Context, data *entity.Answer
info := schema.AdminAnswerInfo{}
info.ID = data.ID
info.QuestionID = data.QuestionID
info.Adopted = data.Adopted
info.Accepted = data.Accepted
info.VoteCount = data.VoteCount
info.CreateTime = data.CreatedAt.Unix()
info.UpdateTime = data.UpdatedAt.Unix()

View File

@ -80,7 +80,7 @@ func (as *AnswerService) RemoveAnswer(ctx context.Context, req *schema.RemoveAns
if answerInfo.VoteCount > 0 {
return errors.BadRequest(reason.AnswerCannotDeleted)
}
if answerInfo.Adopted == schema.AnswerAdoptedEnable {
if answerInfo.Accepted == schema.AnswerAcceptedEnable {
return errors.BadRequest(reason.AnswerCannotDeleted)
}
questionInfo, exist, err := as.questionRepo.GetQuestion(ctx, answerInfo.QuestionID)
@ -138,7 +138,7 @@ func (as *AnswerService) Insert(ctx context.Context, req *schema.AnswerAddReq) (
insertData.UserID = req.UserID
insertData.OriginalText = req.Content
insertData.ParsedText = req.HTML
insertData.Adopted = schema.AnswerAdoptedFailed
insertData.Accepted = schema.AnswerAcceptedFailed
insertData.QuestionID = req.QuestionID
insertData.RevisionID = "0"
insertData.LastEditUserID = "0"
@ -285,8 +285,8 @@ func (as *AnswerService) Update(ctx context.Context, req *schema.AnswerUpdateReq
return insertData.ID, nil
}
// UpdateAdopted
func (as *AnswerService) UpdateAdopted(ctx context.Context, req *schema.AnswerAdoptedReq) error {
// UpdateAccepted
func (as *AnswerService) UpdateAccepted(ctx context.Context, req *schema.AnswerAcceptedReq) error {
if req.AnswerID == "" {
req.AnswerID = "0"
}
@ -330,7 +330,7 @@ func (as *AnswerService) UpdateAdopted(ctx context.Context, req *schema.AnswerAd
}
}
err = as.answerRepo.UpdateAdopted(ctx, req.AnswerID, req.QuestionID)
err = as.answerRepo.UpdateAccepted(ctx, req.AnswerID, req.QuestionID)
if err != nil {
return err
}
@ -413,7 +413,7 @@ func (as *AnswerService) Get(ctx context.Context, answerID, loginUserID string)
}
func (as *AnswerService) AdminSetAnswerStatus(ctx context.Context, req *schema.AdminSetAnswerStatusRequest) error {
setStatus, ok := entity.CmsAnswerSearchStatus[req.StatusStr]
setStatus, ok := entity.AdminAnswerSearchStatus[req.StatusStr]
if !ok {
return fmt.Errorf("question status does not exist")
}

View File

@ -16,9 +16,9 @@ type AuthRepo interface {
SetUserStatus(ctx context.Context, userID string, userInfo *entity.UserCacheInfo) (err error)
GetUserStatus(ctx context.Context, userID string) (userInfo *entity.UserCacheInfo, err error)
RemoveUserStatus(ctx context.Context, userID string) (err error)
GetBackyardUserCacheInfo(ctx context.Context, accessToken string) (userInfo *entity.UserCacheInfo, err error)
SetBackyardUserCacheInfo(ctx context.Context, accessToken string, userInfo *entity.UserCacheInfo) error
RemoveBackyardUserCacheInfo(ctx context.Context, accessToken string) (err error)
GetAdminUserCacheInfo(ctx context.Context, accessToken string) (userInfo *entity.UserCacheInfo, err error)
SetAdminUserCacheInfo(ctx context.Context, accessToken string, userInfo *entity.UserCacheInfo) error
RemoveAdminUserCacheInfo(ctx context.Context, accessToken string) (err error)
AddUserTokenMapping(ctx context.Context, userID, accessToken string) (err error)
RemoveAllUserTokens(ctx context.Context, userID string)
}
@ -90,17 +90,17 @@ func (as *AuthService) RemoveAllUserTokens(ctx context.Context, userID string) {
as.authRepo.RemoveAllUserTokens(ctx, userID)
}
//cms
//Admin
func (as *AuthService) GetCmsUserCacheInfo(ctx context.Context, accessToken string) (userInfo *entity.UserCacheInfo, err error) {
return as.authRepo.GetBackyardUserCacheInfo(ctx, accessToken)
func (as *AuthService) GetAdminUserCacheInfo(ctx context.Context, accessToken string) (userInfo *entity.UserCacheInfo, err error) {
return as.authRepo.GetAdminUserCacheInfo(ctx, accessToken)
}
func (as *AuthService) SetCmsUserCacheInfo(ctx context.Context, accessToken string, userInfo *entity.UserCacheInfo) (err error) {
err = as.authRepo.SetBackyardUserCacheInfo(ctx, accessToken, userInfo)
func (as *AuthService) SetAdminUserCacheInfo(ctx context.Context, accessToken string, userInfo *entity.UserCacheInfo) (err error) {
err = as.authRepo.SetAdminUserCacheInfo(ctx, accessToken, userInfo)
return err
}
func (as *AuthService) RemoveCmsUserCacheInfo(ctx context.Context, accessToken string) (err error) {
return as.authRepo.RemoveBackyardUserCacheInfo(ctx, accessToken)
func (as *AuthService) RemoveAdminUserCacheInfo(ctx context.Context, accessToken string) (err error) {
return as.authRepo.RemoveAdminUserCacheInfo(ctx, accessToken)
}

View File

@ -168,17 +168,11 @@ func (cs *CommentService) AddComment(ctx context.Context, req *schema.AddComment
// RemoveComment delete comment
func (cs *CommentService) RemoveComment(ctx context.Context, req *schema.RemoveCommentReq) (err error) {
if err := cs.checkCommentWhetherOwner(ctx, req.UserID, req.CommentID); err != nil {
return err
}
return cs.commentRepo.RemoveComment(ctx, req.CommentID)
}
// UpdateComment update comment
func (cs *CommentService) UpdateComment(ctx context.Context, req *schema.UpdateCommentReq) (err error) {
if err := cs.checkCommentWhetherOwner(ctx, req.UserID, req.CommentID); err != nil {
return err
}
comment := &entity.Comment{}
_ = copier.Copy(comment, req)
comment.ID = req.CommentID

View File

@ -191,7 +191,7 @@ func (ns *NotificationCommon) SendNotificationToAllFollower(ctx context.Context,
if msg.NotificationAction != constant.UpdateQuestion &&
msg.NotificationAction != constant.AnswerTheQuestion &&
msg.NotificationAction != constant.UpdateAnswer &&
msg.NotificationAction != constant.AdoptAnswer {
msg.NotificationAction != constant.AcceptAnswer {
return
}
condObjectID := msg.ObjectID

View File

@ -20,8 +20,8 @@ import (
"github.com/answerdev/answer/internal/service/rank"
"github.com/answerdev/answer/internal/service/reason"
"github.com/answerdev/answer/internal/service/report"
"github.com/answerdev/answer/internal/service/report_backyard"
"github.com/answerdev/answer/internal/service/report_handle_backyard"
"github.com/answerdev/answer/internal/service/report_admin"
"github.com/answerdev/answer/internal/service/report_handle_admin"
"github.com/answerdev/answer/internal/service/revision_common"
"github.com/answerdev/answer/internal/service/role"
"github.com/answerdev/answer/internal/service/search_parser"
@ -30,7 +30,7 @@ import (
"github.com/answerdev/answer/internal/service/tag"
tagcommon "github.com/answerdev/answer/internal/service/tag_common"
"github.com/answerdev/answer/internal/service/uploader"
"github.com/answerdev/answer/internal/service/user_backyard"
"github.com/answerdev/answer/internal/service/user_admin"
usercommon "github.com/answerdev/answer/internal/service/user_common"
"github.com/google/wire"
)
@ -64,9 +64,9 @@ var ProviderSetService = wire.NewSet(
NewSearchService,
meta.NewMetaService,
object_info.NewObjService,
report_handle_backyard.NewReportHandle,
report_backyard.NewReportBackyardService,
user_backyard.NewUserBackyardService,
report_handle_admin.NewReportHandle,
report_admin.NewReportAdminService,
user_admin.NewUserAdminService,
reason.NewReasonService,
siteinfo_common.NewSiteInfoCommonService,
siteinfo.NewSiteInfoService,

View File

@ -40,7 +40,7 @@ type QuestionRepo interface {
UpdateAccepted(ctx context.Context, question *entity.Question) (err error)
UpdateLastAnswer(ctx context.Context, question *entity.Question) (err error)
FindByID(ctx context.Context, id []string) (questionList []*entity.Question, err error)
CmsSearchList(ctx context.Context, search *schema.CmsQuestionSearch) ([]*entity.Question, int64, error)
AdminSearchList(ctx context.Context, search *schema.AdminQuestionSearch) ([]*entity.Question, int64, error)
GetQuestionCount(ctx context.Context) (count int64, err error)
GetQuestionIDsPage(ctx context.Context, page, pageSize int) (questionIDList []*schema.SiteMapQuestionInfo, err error)
}

View File

@ -158,9 +158,66 @@ func (qs *QuestionService) AddQuestionCheckTags(ctx context.Context, Tags []*ent
}
return []string{}, nil
}
func (qs *QuestionService) CheckAddQuestion(ctx context.Context, req *schema.QuestionAdd) (errorlist any, err error) {
if len(req.Tags) == 0 {
errorlist := make([]*validator.FormErrorField, 0)
errorlist = append(errorlist, &validator.FormErrorField{
ErrorField: "tags",
ErrorMsg: translator.GlobalTrans.Tr(handler.GetLangByCtx(ctx), reason.TagNotFound),
})
err = errors.BadRequest(reason.RecommendTagEnter)
return errorlist, err
}
recommendExist, err := qs.tagCommon.ExistRecommend(ctx, req.Tags)
if err != nil {
return
}
if !recommendExist {
errorlist := make([]*validator.FormErrorField, 0)
errorlist = append(errorlist, &validator.FormErrorField{
ErrorField: "tags",
ErrorMsg: translator.GlobalTrans.Tr(handler.GetLangByCtx(ctx), reason.RecommendTagEnter),
})
err = errors.BadRequest(reason.RecommendTagEnter)
return errorlist, err
}
tagNameList := make([]string, 0)
for _, tag := range req.Tags {
tagNameList = append(tagNameList, tag.SlugName)
}
Tags, tagerr := qs.tagCommon.GetTagListByNames(ctx, tagNameList)
if tagerr != nil {
return errorlist, tagerr
}
if !req.QuestionPermission.CanUseReservedTag {
taglist, err := qs.AddQuestionCheckTags(ctx, Tags)
errMsg := fmt.Sprintf(`"%s" can only be used by moderators.`,
strings.Join(taglist, ","))
if err != nil {
errorlist := make([]*validator.FormErrorField, 0)
errorlist = append(errorlist, &validator.FormErrorField{
ErrorField: "tags",
ErrorMsg: errMsg,
})
err = errors.BadRequest(reason.RecommendTagEnter)
return errorlist, err
}
}
return nil, nil
}
// AddQuestion add question
func (qs *QuestionService) AddQuestion(ctx context.Context, req *schema.QuestionAdd) (questionInfo any, err error) {
if len(req.Tags) == 0 {
errorlist := make([]*validator.FormErrorField, 0)
errorlist = append(errorlist, &validator.FormErrorField{
ErrorField: "tags",
ErrorMsg: translator.GlobalTrans.Tr(handler.GetLangByCtx(ctx), reason.TagNotFound),
})
err = errors.BadRequest(reason.RecommendTagEnter)
return errorlist, err
}
recommendExist, err := qs.tagCommon.ExistRecommend(ctx, req.Tags)
if err != nil {
return
@ -610,7 +667,7 @@ func (qs *QuestionService) SearchUserList(ctx context.Context, userName, order s
for _, item := range questionlist {
info := &schema.UserQuestionInfo{}
_ = copier.Copy(info, item)
status, ok := entity.CmsQuestionSearchStatusIntToString[item.Status]
status, ok := entity.AdminQuestionSearchStatusIntToString[item.Status]
if ok {
info.Status = status
}
@ -787,7 +844,7 @@ func (qs *QuestionService) SearchByTitleLike(ctx context.Context, title string,
item.AnswerCount = question.AnswerCount
item.CollectionCount = question.CollectionCount
item.FollowCount = question.FollowCount
status, ok := entity.CmsQuestionSearchStatusIntToString[question.Status]
status, ok := entity.AdminQuestionSearchStatusIntToString[question.Status]
if ok {
item.Status = status
}
@ -855,7 +912,7 @@ func (qs *QuestionService) SearchList(ctx context.Context, req *schema.QuestionS
}
func (qs *QuestionService) AdminSetQuestionStatus(ctx context.Context, questionID string, setStatusStr string) error {
setStatus, ok := entity.CmsQuestionSearchStatus[setStatusStr]
setStatus, ok := entity.AdminQuestionSearchStatus[setStatusStr]
if !ok {
return fmt.Errorf("question status does not exist")
}
@ -910,10 +967,10 @@ func (qs *QuestionService) AdminSetQuestionStatus(ctx context.Context, questionI
return nil
}
func (qs *QuestionService) CmsSearchList(ctx context.Context, search *schema.CmsQuestionSearch, loginUserID string) ([]*schema.AdminQuestionInfo, int64, error) {
func (qs *QuestionService) AdminSearchList(ctx context.Context, search *schema.AdminQuestionSearch, loginUserID string) ([]*schema.AdminQuestionInfo, int64, error) {
list := make([]*schema.AdminQuestionInfo, 0)
status, ok := entity.CmsQuestionSearchStatus[search.StatusStr]
status, ok := entity.AdminQuestionSearchStatus[search.StatusStr]
if ok {
search.Status = status
}
@ -921,7 +978,7 @@ func (qs *QuestionService) CmsSearchList(ctx context.Context, search *schema.Cms
if search.Status == 0 {
search.Status = 1
}
dblist, count, err := qs.questionRepo.CmsSearchList(ctx, search)
dblist, count, err := qs.questionRepo.AdminSearchList(ctx, search)
if err != nil {
return list, count, err
}
@ -949,11 +1006,11 @@ func (qs *QuestionService) CmsSearchList(ctx context.Context, search *schema.Cms
return list, count, nil
}
// CmsSearchList
func (qs *QuestionService) CmsSearchAnswerList(ctx context.Context, search *entity.CmsAnswerSearch, loginUserID string) ([]*schema.AdminAnswerInfo, int64, error) {
// AdminSearchList
func (qs *QuestionService) AdminSearchAnswerList(ctx context.Context, search *entity.AdminAnswerSearch, loginUserID string) ([]*schema.AdminAnswerInfo, int64, error) {
answerlist := make([]*schema.AdminAnswerInfo, 0)
status, ok := entity.CmsAnswerSearchStatus[search.StatusStr]
status, ok := entity.AdminAnswerSearchStatus[search.StatusStr]
if ok {
search.Status = status
}
@ -961,7 +1018,7 @@ func (qs *QuestionService) CmsSearchAnswerList(ctx context.Context, search *enti
if search.Status == 0 {
search.Status = 1
}
dblist, count, err := qs.questioncommon.AnswerCommon.CmsSearchList(ctx, search)
dblist, count, err := qs.questioncommon.AnswerCommon.AdminSearchList(ctx, search)
if err != nil {
return answerlist, count, err
}

View File

@ -1,4 +1,4 @@
package report_backyard
package report_admin
import (
"context"
@ -16,35 +16,35 @@ import (
"github.com/answerdev/answer/internal/service/comment_common"
questioncommon "github.com/answerdev/answer/internal/service/question_common"
"github.com/answerdev/answer/internal/service/report_common"
"github.com/answerdev/answer/internal/service/report_handle_backyard"
"github.com/answerdev/answer/internal/service/report_handle_admin"
usercommon "github.com/answerdev/answer/internal/service/user_common"
"github.com/jinzhu/copier"
"github.com/segmentfault/pacman/errors"
)
// ReportBackyardService user service
type ReportBackyardService struct {
// ReportAdminService user service
type ReportAdminService struct {
reportRepo report_common.ReportRepo
commonUser *usercommon.UserCommon
commonRepo *common.CommonRepo
answerRepo answercommon.AnswerRepo
questionRepo questioncommon.QuestionRepo
commentCommonRepo comment_common.CommentCommonRepo
reportHandle *report_handle_backyard.ReportHandle
reportHandle *report_handle_admin.ReportHandle
configRepo config.ConfigRepo
}
// NewReportBackyardService new report service
func NewReportBackyardService(
// NewReportAdminService new report service
func NewReportAdminService(
reportRepo report_common.ReportRepo,
commonUser *usercommon.UserCommon,
commonRepo *common.CommonRepo,
answerRepo answercommon.AnswerRepo,
questionRepo questioncommon.QuestionRepo,
commentCommonRepo comment_common.CommentCommonRepo,
reportHandle *report_handle_backyard.ReportHandle,
configRepo config.ConfigRepo) *ReportBackyardService {
return &ReportBackyardService{
reportHandle *report_handle_admin.ReportHandle,
configRepo config.ConfigRepo) *ReportAdminService {
return &ReportAdminService{
reportRepo: reportRepo,
commonUser: commonUser,
commonRepo: commonRepo,
@ -57,7 +57,7 @@ func NewReportBackyardService(
}
// ListReportPage list report pages
func (rs *ReportBackyardService) ListReportPage(ctx context.Context, dto schema.GetReportListPageDTO) (pageModel *pager.PageModel, err error) {
func (rs *ReportAdminService) ListReportPage(ctx context.Context, dto schema.GetReportListPageDTO) (pageModel *pager.PageModel, err error) {
var (
resp []*schema.GetReportListPageResp
flags []entity.Report
@ -105,7 +105,7 @@ func (rs *ReportBackyardService) ListReportPage(ctx context.Context, dto schema.
}
// HandleReported handle the reported object
func (rs *ReportBackyardService) HandleReported(ctx context.Context, req schema.ReportHandleReq) (err error) {
func (rs *ReportAdminService) HandleReported(ctx context.Context, req schema.ReportHandleReq) (err error) {
var (
reported *entity.Report
handleData = entity.Report{
@ -139,7 +139,7 @@ func (rs *ReportBackyardService) HandleReported(ctx context.Context, req schema.
return
}
func (rs *ReportBackyardService) parseObject(ctx context.Context, resp *[]*schema.GetReportListPageResp) {
func (rs *ReportAdminService) parseObject(ctx context.Context, resp *[]*schema.GetReportListPageResp) {
var (
res = *resp
)

View File

@ -1,4 +1,4 @@
package report_handle_backyard
package report_handle_admin
import (
"context"

View File

@ -114,23 +114,10 @@ func (s *SiteInfoService) SaveSiteGeneral(ctx context.Context, req schema.SiteGe
func (s *SiteInfoService) SaveSiteInterface(ctx context.Context, req schema.SiteInterfaceReq) (err error) {
var (
siteType = "interface"
themeExist bool
content []byte
siteType = "interface"
content []byte
)
// check theme
for _, theme := range schema.GetThemeOptions {
if theme.Value == req.Theme {
themeExist = true
break
}
}
if !themeExist {
err = errors.BadRequest(reason.ThemeNotFound)
return
}
// check language
if !translator.CheckLanguageIsValid(req.Language) {
err = errors.BadRequest(reason.LangNotFound)

View File

@ -320,13 +320,13 @@ func (ts *TagService) GetTagWithPage(ctx context.Context, req *schema.GetTagWith
resp := make([]*schema.GetTagPageResp, 0)
for _, tag := range tags {
excerpt := htmltext.FetchExcerpt(tag.ParsedText, "...", 240)
//excerpt := htmltext.FetchExcerpt(tag.ParsedText, "...", 240)
resp = append(resp, &schema.GetTagPageResp{
TagID: tag.ID,
SlugName: tag.SlugName,
DisplayName: tag.DisplayName,
OriginalText: excerpt,
ParsedText: excerpt,
OriginalText: tag.OriginalText,
ParsedText: tag.ParsedText,
FollowCount: tag.FollowCount,
QuestionCount: tag.QuestionCount,
IsFollower: ts.checkTagIsFollow(ctx, req.UserID, tag.ID),

View File

@ -661,7 +661,9 @@ func (ts *TagCommonService) UpdateTag(ctx context.Context, req *schema.UpdateTag
return errors.BadRequest(reason.TagNotFound)
}
//If the content is the same, ignore it
if tagInfo.OriginalText == req.OriginalText {
if tagInfo.OriginalText == req.OriginalText &&
tagInfo.DisplayName == req.DisplayName &&
tagInfo.SlugName == req.SlugName {
return nil
}

View File

@ -1,4 +1,4 @@
package user_backyard
package user_admin
import (
"context"
@ -21,8 +21,8 @@ import (
"golang.org/x/crypto/bcrypt"
)
// UserBackyardRepo user repository
type UserBackyardRepo interface {
// UserAdminRepo user repository
type UserAdminRepo interface {
UpdateUserStatus(ctx context.Context, userID string, userStatus, mailStatus int, email string) (err error)
GetUserInfo(ctx context.Context, userID string) (user *entity.User, exist bool, err error)
GetUserInfoByEmail(ctx context.Context, email string) (user *entity.User, exist bool, err error)
@ -32,22 +32,22 @@ type UserBackyardRepo interface {
UpdateUserPassword(ctx context.Context, userID string, password string) (err error)
}
// UserBackyardService user service
type UserBackyardService struct {
userRepo UserBackyardRepo
// UserAdminService user service
type UserAdminService struct {
userRepo UserAdminRepo
userRoleRelService *role.UserRoleRelService
authService *auth.AuthService
userCommonService *usercommon.UserCommon
}
// NewUserBackyardService new user backyard service
func NewUserBackyardService(
userRepo UserBackyardRepo,
// NewUserAdminService new user admin service
func NewUserAdminService(
userRepo UserAdminRepo,
userRoleRelService *role.UserRoleRelService,
authService *auth.AuthService,
userCommonService *usercommon.UserCommon,
) *UserBackyardService {
return &UserBackyardService{
) *UserAdminService {
return &UserAdminService{
userRepo: userRepo,
userRoleRelService: userRoleRelService,
authService: authService,
@ -56,7 +56,7 @@ func NewUserBackyardService(
}
// UpdateUserStatus update user
func (us *UserBackyardService) UpdateUserStatus(ctx context.Context, req *schema.UpdateUserStatusReq) (err error) {
func (us *UserAdminService) UpdateUserStatus(ctx context.Context, req *schema.UpdateUserStatusReq) (err error) {
userInfo, exist, err := us.userRepo.GetUserInfo(ctx, req.UserID)
if err != nil {
return
@ -87,7 +87,7 @@ func (us *UserBackyardService) UpdateUserStatus(ctx context.Context, req *schema
}
// UpdateUserRole update user role
func (us *UserBackyardService) UpdateUserRole(ctx context.Context, req *schema.UpdateUserRoleReq) (err error) {
func (us *UserAdminService) UpdateUserRole(ctx context.Context, req *schema.UpdateUserRoleReq) (err error) {
// Users cannot modify their roles
if req.UserID == req.LoginUserID {
return errors.BadRequest(reason.UserCannotUpdateYourRole)
@ -103,7 +103,7 @@ func (us *UserBackyardService) UpdateUserRole(ctx context.Context, req *schema.U
}
// AddUser add user
func (us *UserBackyardService) AddUser(ctx context.Context, req *schema.AddUserReq) (err error) {
func (us *UserAdminService) AddUser(ctx context.Context, req *schema.AddUserReq) (err error) {
_, has, err := us.userRepo.GetUserInfoByEmail(ctx, req.Email)
if err != nil {
return err
@ -138,7 +138,7 @@ func (us *UserBackyardService) AddUser(ctx context.Context, req *schema.AddUserR
}
// UpdateUserPassword update user password
func (us *UserBackyardService) UpdateUserPassword(ctx context.Context, req *schema.UpdateUserPasswordReq) (err error) {
func (us *UserAdminService) UpdateUserPassword(ctx context.Context, req *schema.UpdateUserPasswordReq) (err error) {
userInfo, exist, err := us.userRepo.GetUserInfo(ctx, req.UserID)
if err != nil {
return err
@ -162,7 +162,7 @@ func (us *UserBackyardService) UpdateUserPassword(ctx context.Context, req *sche
}
// GetUserInfo get user one
func (us *UserBackyardService) GetUserInfo(ctx context.Context, userID string) (resp *schema.GetUserInfoResp, err error) {
func (us *UserAdminService) GetUserInfo(ctx context.Context, userID string) (resp *schema.GetUserInfoResp, err error) {
user, exist, err := us.userRepo.GetUserInfo(ctx, userID)
if err != nil {
return
@ -177,7 +177,7 @@ func (us *UserBackyardService) GetUserInfo(ctx context.Context, userID string) (
}
// GetUserPage get user list page
func (us *UserBackyardService) GetUserPage(ctx context.Context, req *schema.GetUserPageReq) (pageModel *pager.PageModel, err error) {
func (us *UserAdminService) GetUserPage(ctx context.Context, req *schema.GetUserPageReq) (pageModel *pager.PageModel, err error) {
user := &entity.User{}
_ = copier.Copy(user, req)
@ -246,7 +246,7 @@ func (us *UserBackyardService) GetUserPage(ctx context.Context, req *schema.GetU
return pager.NewPageModel(total, resp), nil
}
func (us *UserBackyardService) setUserRoleInfo(ctx context.Context, resp []*schema.GetUserPageResp) {
func (us *UserAdminService) setUserRoleInfo(ctx context.Context, resp []*schema.GetUserPageResp) {
var userIDs []string
for _, u := range resp {
userIDs = append(userIDs, u.UserID)

View File

@ -138,7 +138,7 @@ func (us *UserService) EmailLogin(ctx context.Context, req *schema.UserEmailLogi
}
resp.IsAdmin = userCacheInfo.IsAdmin
if resp.IsAdmin {
err = us.authService.SetCmsUserCacheInfo(ctx, resp.AccessToken, userCacheInfo)
err = us.authService.SetAdminUserCacheInfo(ctx, resp.AccessToken, userCacheInfo)
if err != nil {
return nil, err
}
@ -355,7 +355,7 @@ func (us *UserService) UserRegisterByEmail(ctx context.Context, registerUserInfo
}
resp.IsAdmin = userCacheInfo.IsAdmin
if resp.IsAdmin {
err = us.authService.SetCmsUserCacheInfo(ctx, resp.AccessToken, &entity.UserCacheInfo{UserID: userInfo.ID})
err = us.authService.SetAdminUserCacheInfo(ctx, resp.AccessToken, &entity.UserCacheInfo{UserID: userInfo.ID})
if err != nil {
return nil, err
}
@ -451,7 +451,7 @@ func (us *UserService) UserVerifyEmail(ctx context.Context, req *schema.UserVeri
}
resp.IsAdmin = userCacheInfo.IsAdmin
if resp.IsAdmin {
err = us.authService.SetCmsUserCacheInfo(ctx, resp.AccessToken, &entity.UserCacheInfo{UserID: userInfo.ID})
err = us.authService.SetAdminUserCacheInfo(ctx, resp.AccessToken, &entity.UserCacheInfo{UserID: userInfo.ID})
if err != nil {
return nil, err
}

View File

@ -1,11 +1,30 @@
package converter
import (
"github.com/gomarkdown/markdown"
"bytes"
"github.com/segmentfault/pacman/log"
"github.com/yuin/goldmark"
"github.com/yuin/goldmark/extension"
"github.com/yuin/goldmark/parser"
"github.com/yuin/goldmark/renderer/html"
)
// Markdown2HTML convert markdown to html
func Markdown2HTML(md string) string {
html := markdown.ToHTML([]byte(md), nil, nil)
return string(html)
func Markdown2HTML(source string) string {
mdConverter := goldmark.New(
goldmark.WithExtensions(extension.GFM),
goldmark.WithParserOptions(
parser.WithAutoHeadingID(),
),
goldmark.WithRendererOptions(
html.WithHardWraps(),
),
)
var buf bytes.Buffer
if err := mdConverter.Convert([]byte(source), &buf); err != nil {
log.Error(err)
return source
}
return buf.String()
}

View File

@ -102,9 +102,9 @@ All our translations are placed in the i18n directory.
## 📱Environment Support
| [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt="Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)<br>Edge | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)<br>Firefox | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)<br>Chrome | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)<br>Safari |
| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| last 2 versions | last 2 versions | last 2 versions | last 2 versions |
| [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt="Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)<br />Edge | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)<br />Firefox | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)<br />Chrome | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)<br />Safari |
|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| last 2 versions | last 2 versions | last 2 versions | last 2 versions |
## ⭐ Built with
- [TypeScript](https://www.typescriptlang.org/) - strongly typed JavaScript

View File

@ -1,44 +1 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<meta name="theme-color" content="#000000" />
<link rel="manifest" href="/manifest.json" />
<title>Answer</title>
<script defer="defer" src="/static/js/main.9de9552b.js"></script>
<link href="/static/css/main.d4180d41.css" rel="stylesheet">
<link href="/custom.css" rel="stylesheet">
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root">
<style>
@keyframes _doc-spin {
to {
transform: rotate(360deg);
}
}
#doc-spinner {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
#doc-spinner .spinner {
box-sizing: border-box;
display: inline-block;
width: 2rem;
height: 2rem;
vertical-align: -0.125em;
border: 0.25rem solid currentColor;
border-right-color: transparent;
color: rgba(108, 117, 125, 0.75);
border-radius: 50%;
animation: 0.75s linear infinite _doc-spin;
}
</style>
<div id="doc-spinner"><div class="spinner"></div></div>
</div>
</body>
</html>
<!doctype html><html><head><meta charset="utf-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><link rel="manifest" href="/manifest.json"/><script defer="defer" src="/static/js/main.554b9f62.js"></script><link href="/static/css/main.401dc3ca.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"><div id="spin-mask" hidden><script>try{document.querySelector("#spin-mask").removeAttribute("hidden")}catch(e){}</script><style>@keyframes _doc-spin{to{transform:rotate(360deg)}}#spin-mask{position:fixed;top:0;right:0;bottom:0;left:0;background-color:#fff;z-index:9999}#spin-container{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)}#spin-container .spinner{box-sizing:border-box;display:inline-block;width:2rem;height:2rem;vertical-align:-.125em;border:.25rem solid currentColor;border-right-color:transparent;color:rgba(108,117,125,.75);border-radius:50%;animation:.75s linear infinite _doc-spin}</style><div id="spin-container"><div class="spinner"></div></div></div></div></body></html>

View File

@ -29,18 +29,18 @@ module.exports = {
devServer: function(configFunction) {
return function(proxy, allowedHost) {
const config = configFunction(proxy, allowedHost);
config.proxy = {
'/answer': {
config.proxy = [
{
context: ['/answer', '/installation'],
target: process.env.REACT_APP_API_URL,
changeOrigin: true,
secure: false,
},
'/installation': {
{
context: ['/custom.css'],
target: process.env.REACT_APP_API_URL,
changeOrigin: true,
secure: false,
},
};
}
];
return config;
};
}

View File

@ -9,12 +9,14 @@
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root">
<div id="spin-mask" hidden>
<script>
try {
document.querySelector('#spin-mask').removeAttribute('hidden');
} catch (ex){}
</script>
<div id="spin-mask">
<noscript>
<style>
#spin-mask {
display: none !important;
}
</style>
</noscript>
<style>
@keyframes _doc-spin {
to { transform: rotate(360deg) }

View File

@ -10,6 +10,39 @@ const makeMarker = (mark) => {
return `<!--${mark}-->`;
};
const ActivateScriptNodes = (el, part) => {
let startMarkNode;
const scriptList: HTMLScriptElement[] = [];
const { childNodes } = el;
for (let i = 0; i < childNodes.length; i += 1) {
const node = childNodes[i];
if (node.nodeType === 8 && node.nodeValue === part) {
if (!startMarkNode) {
startMarkNode = node;
} else {
// this is the endMarkNode
break;
}
}
if (
startMarkNode &&
node.nodeType === 1 &&
node.nodeName.toLowerCase() === 'script'
) {
scriptList.push(node);
}
}
scriptList.forEach((so) => {
const script = document.createElement('script');
script.text = so.text;
for (let i = 0; i < so.attributes.length; i += 1) {
const attr = so.attributes[i];
script.setAttribute(attr.name, attr.value);
}
el.replaceChild(script, so);
});
};
type pos = 'afterbegin' | 'beforeend';
const renderCustomArea = (el, part, pos: pos, content: string = '') => {
let startMarkNode;
@ -43,6 +76,7 @@ const renderCustomArea = (el, part, pos: pos, content: string = '') => {
el.insertAdjacentHTML(pos, makeMarker(part));
el.insertAdjacentHTML(pos, content);
el.insertAdjacentHTML(pos, makeMarker(part));
ActivateScriptNodes(el, part);
};
const handleCustomHead = (content) => {
const el = document.head;
@ -70,12 +104,7 @@ const Index: FC = () => {
handleCustomHeader(custom_header);
handleCustomFooter(custom_footer);
}, [custom_head, custom_header, custom_footer]);
return (
<>
{null}
{/* App customize */}
</>
);
return null;
};
export default memo(Index);

View File

@ -9,7 +9,7 @@ import { themeSettingStore } from '@/stores';
const Index: FC = () => {
const { theme, theme_config } = themeSettingStore((_) => _);
let primaryColor;
if (theme_config[theme]?.primary_color) {
if (theme_config?.[theme]?.primary_color) {
primaryColor = Color(theme_config[theme].primary_color);
}

View File

@ -1,6 +1,6 @@
import { FC, memo } from 'react';
import clssnames from 'classnames';
import classnames from 'classnames';
import { Tag } from '@/components';
import { diffText } from '@/utils';
@ -90,7 +90,7 @@ const Index: FC<Props> = ({
)}
{objectType === 'tag' && opts?.showTagUrlSlug && (
<div
className={clssnames(
className={classnames(
'fs-14 font-monospace',
newData.original_text && 'mb-4',
)}>

View File

@ -1,7 +1,6 @@
import { FC, memo, useState } from 'react';
import { Card, Button } from 'react-bootstrap';
import { useTranslation } from 'react-i18next';
import { NavLink } from 'react-router-dom';
import { TagSelector, Tag } from '@/components';
import { tryLoggedAndActivated } from '@/utils/guard';
@ -78,11 +77,13 @@ const Index: FC = () => {
) : (
<>
<div className="text-muted">{t('follow_tag_tip')}</div>
<NavLink className="d-inline-block mt-3" to="/tags">
<Button size="sm" variant="outline-primary">
{t('follow_a_tag')}
</Button>
</NavLink>
<Button
size="sm"
className="mt-3"
variant="outline-primary"
onClick={() => setEditState(true)}>
{t('follow_a_tag')}
</Button>
</>
)}
</Card.Body>

View File

@ -72,17 +72,17 @@ const Header: FC = () => {
}
}, [location.pathname]);
let navbarStyle = 'theme-colored';
const { theme, theme_config } = themeSettingStore((_) => _);
let themeType = 'theme-colored';
if (theme && theme_config[theme]) {
themeType = `theme-${theme_config[theme].navbar_style}`;
if (theme_config?.[theme]?.navbar_style) {
navbarStyle = `theme-${theme_config[theme].navbar_style}`;
}
return (
<Navbar
variant={themeType === 'theme-colored' ? 'dark' : ''}
variant={navbarStyle === 'theme-colored' ? 'dark' : ''}
expand="lg"
className={classnames('sticky-top', themeType)}
className={classnames('sticky-top', navbarStyle)}
id="header">
<Container className="d-flex align-items-center">
<Navbar.Toggle
@ -124,7 +124,7 @@ const Header: FC = () => {
{loginSetting.allow_new_registrations && (
<Button
variant={
themeType === 'theme-colored' ? 'light' : 'primary'
navbarStyle === 'theme-colored' ? 'light' : 'primary'
}
href="/users/register">
{t('btns.signup')}
@ -181,8 +181,8 @@ const Header: FC = () => {
<Link
to="/questions/ask"
className={classnames('text-capitalize text-nowrap btn', {
'btn-light': themeType !== 'theme-light',
'btn-primary': themeType === 'theme-light',
'btn-light': navbarStyle !== 'theme-light',
'btn-primary': navbarStyle === 'theme-light',
})}>
{t('btns.add_question')}
</Link>
@ -199,8 +199,8 @@ const Header: FC = () => {
<Button
variant="link"
className={classnames('me-2', {
'link-light': themeType === 'theme-colored',
'link-primary': themeType !== 'theme-colored',
'link-light': navbarStyle === 'theme-colored',
'link-primary': navbarStyle !== 'theme-colored',
})}
href="/users/login">
{t('btns.login')}
@ -208,7 +208,7 @@ const Header: FC = () => {
{loginSetting.allow_new_registrations && (
<Button
variant={
themeType === 'theme-colored' ? 'light' : 'primary'
navbarStyle === 'theme-colored' ? 'light' : 'primary'
}
href="/users/register">
{t('btns.signup')}

View File

@ -117,12 +117,12 @@ const QuestionList: FC<Props> = ({ source }) => {
i18nKeyPrefix="question"
/>
</div>
<ListGroup variant="flush" className="border-top border-bottom-0">
<ListGroup className="rounded-0">
{listData?.list?.map((li) => {
return (
<ListGroup.Item
key={li.id}
className="border-bottom bg-transparent py-3 px-0">
className="bg-transparent py-3 px-0 border-start-0 border-end-0">
<h5 className="text-wrap text-break">
<NavLink
to={pathFactory.questionLanding(li.id, li.url_title)}

View File

@ -22,7 +22,6 @@ const Index: FC<IProps> = ({ type, qid, aid, title, slugTitle = '' }) => {
const [showTip, setShowTip] = useState(false);
const [canSystemShare, setSystemShareState] = useState(false);
const { t } = useTranslation();
// FIXME: pathFactory
let baseUrl =
type === 'question'
? `${window.location.origin}${pathFactory.questionLanding(
@ -46,7 +45,11 @@ const Index: FC<IProps> = ({ type, qid, aid, title, slugTitle = '' }) => {
const handleCopy = (evt) => {
evt.preventDefault();
evt.stopPropagation();
copy(baseUrl);
let copyText = baseUrl;
if (title) {
copyText = `${title} ${baseUrl}`;
}
copy(copyText);
setShowTip(true);
setTimeout(closeShare, 1000);
};

View File

@ -56,7 +56,7 @@ const Answers: FC = () => {
Modal.confirm({
title: t('title', { keyPrefix: 'delete' }),
content:
item.adopted === 2
item.accepted === 2
? t('answer_accepted', { keyPrefix: 'delete' })
: `<p>${t('other', { keyPrefix: 'delete' })}</p>`,
cancelBtnVariant: 'link',
@ -138,7 +138,7 @@ const Answers: FC = () => {
rel="noreferrer">
{li.question_info.title}
</a>
{li.adopted === 2 && (
{li.accepted === 2 && (
<Icon
name="check-circle-fill"
className="ms-2 text-success"

View File

@ -23,7 +23,7 @@ const Index: FC = () => {
description: t('themes.text'),
enum: themeSetting?.theme_options?.map((_) => _.value),
enumNames: themeSetting?.theme_options?.map((_) => _.label),
default: 'default',
default: themeSetting?.theme_options?.[0]?.value,
},
navbar_style: {
type: 'string',
@ -31,12 +31,13 @@ const Index: FC = () => {
description: t('navbar_style.text'),
enum: ['colored', 'light'],
enumNames: ['Colored', 'Light'],
default: 'colored',
},
primary_color: {
type: 'string',
title: t('primary_color.label'),
description: t('primary_color.text'),
default: '#ffffff',
default: '#0033FF',
},
},
};
@ -68,6 +69,7 @@ const Index: FC = () => {
},
},
};
putThemeSetting(reqParams)
.then(() => {
Toast.onShow({

View File

@ -44,7 +44,7 @@ const Index: FC<Props> = ({
const acceptAnswer = () => {
acceptanceAnswer({
question_id: data.question_id,
answer_id: data.adopted === 2 ? '0' : data.id,
answer_id: data.accepted === 2 ? '0' : data.id,
}).then(() => {
callback?.('');
});
@ -85,7 +85,7 @@ const Index: FC<Props> = ({
}}
/>
{data?.adopted === 2 && (
{data?.accepted === 2 && (
<Button
disabled={!isAuthor}
variant="outline-success"
@ -96,7 +96,7 @@ const Index: FC<Props> = ({
</Button>
)}
{isAuthor && data.adopted === 1 && (
{isAuthor && data.accepted === 1 && (
<Button
variant="outline-success"
className="ms-3"
@ -114,7 +114,7 @@ const Index: FC<Props> = ({
aid={data.id}
memberActions={data?.member_actions}
type="answer"
isAccepted={data.adopted === 2}
isAccepted={data.accepted === 2}
title={questionTitle}
slugTitle={slugTitle}
callback={callback}

View File

@ -37,7 +37,7 @@ const Index: FC<Props> = ({ data }) => {
<div className="mb-5">
<h3 className="mb-3">{t('title')}</h3>
<p>
<span className="text-secondary">{t('keywords')}</span>
<span className="text-secondary me-1">{t('keywords')}</span>
{q?.replace(reg, '')}
<br />
{options?.length && (

View File

@ -1,5 +1,4 @@
import { FC, memo } from 'react';
import { ListGroupItem } from 'react-bootstrap';
import { useTranslation } from 'react-i18next';
import { QueryGroup } from '@/components';
@ -16,7 +15,7 @@ const Index: FC<Props> = ({ sort, count = 0 }) => {
});
return (
<ListGroupItem className="d-flex flex-wrap align-items-center justify-content-between divide-line pb-3 border-bottom px-0">
<div className="d-flex flex-wrap align-items-center justify-content-between pt-2 pb-3">
<h5 className="mb-0">{t('counts', { count, keyPrefix: 'search' })}</h5>
<QueryGroup
data={sortBtns}
@ -24,7 +23,7 @@ const Index: FC<Props> = ({ sort, count = 0 }) => {
sortKey="order"
i18nKeyPrefix="search.sort_btns"
/>
</ListGroupItem>
</div>
);
};

View File

@ -27,7 +27,7 @@ const Index: FC<Props> = ({ data }) => {
});
}
return (
<ListGroupItem className="py-3 px-0">
<ListGroupItem className="py-3 px-0 border-start-0 border-end-0 bg-transparent">
<div className="mb-2 clearfix">
<span
className="float-start me-2 badge text-bg-dark"

View File

@ -36,10 +36,8 @@ const Index = () => {
<Row className="justify-content-center">
<Col xxl={7} lg={8} sm={12} className="mb-3">
<Head data={extra} />
<ListGroup variant="flush" className="mb-5">
<SearchHead sort={order} count={count} />
<SearchHead sort={order} count={count} />
<ListGroup className="rounded-0 mb-5">
{list?.map((item) => {
return <SearchItem key={item.object.id} data={item} />;
})}

View File

@ -81,7 +81,7 @@ const Tags = () => {
<Card.Body className="d-flex flex-column align-items-start">
<Tag className="mb-3" data={tag} />
<p className="fs-14 flex-fill text-break text-wrap text-truncate-4">
<p className="fs-14 flex-fill text-break text-wrap text-truncate-3">
{tag.original_text}
</p>
<div className="d-flex align-items-center">

View File

@ -16,9 +16,7 @@ const Achievements = ({ data, handleReadNotification }) => {
return <Empty />;
}
return (
<ListGroup
className="border-top border-bottom achievement-wrap"
variant="flush">
<ListGroup className="achievement-wrap rounded-0">
{data.map((item) => {
const { comment, question, answer } =
item?.object_info?.object_map || {};
@ -39,7 +37,10 @@ const Achievements = ({ data, handleReadNotification }) => {
return (
<ListGroup.Item
key={item.id}
className={classNames('d-flex', !item.is_read && 'warning')}>
className={classNames(
'd-flex border-start-0 border-end-0',
!item.is_read && 'warning',
)}>
{item.rank > 0 && (
<div className="text-success num text-end">{`+${item.rank}`}</div>
)}

View File

@ -14,7 +14,7 @@ const Inbox = ({ data, handleReadNotification }) => {
return <Empty />;
}
return (
<ListGroup className="border-top border-bottom" variant="flush">
<ListGroup className="rounded-0">
{data.map((item) => {
const { comment, question, answer } =
item?.object_info?.object_map || {};
@ -35,7 +35,10 @@ const Inbox = ({ data, handleReadNotification }) => {
return (
<ListGroup.Item
key={item.id}
className={classNames('py-3', !item.is_read && 'warning')}>
className={classNames(
'py-3 border-start-0 border-end-0',
!item.is_read && 'warning',
)}>
<div>
{item.user_info.status !== 'deleted' ? (
<Link to={`/users/${item.user_info.username}`}>

View File

@ -15,10 +15,12 @@ const Index: FC<Props> = ({ visible, data }) => {
return null;
}
return (
<ListGroup variant="flush">
<ListGroup className="rounded-0">
{data.map((item) => {
return (
<ListGroupItem className="py-3 px-0" key={item.answer_id}>
<ListGroupItem
className="py-3 px-0 bg-transparent border-start-0 border-end-0"
key={item.answer_id}>
<h6 className="mb-2">
<a
href={pathFactory.answerLanding({
@ -42,7 +44,7 @@ const Index: FC<Props> = ({ visible, data }) => {
<span>{item?.vote_count}</span>
</div>
{item.adopted === 2 && (
{item.accepted === 2 && (
<div className="d-flex align-items-center me-3 text-success">
<Icon name="check-circle-fill me-1" />
<span>{t('accepted')}</span>

View File

@ -14,10 +14,12 @@ const Index: FC<Props> = ({ visible, data }) => {
return null;
}
return (
<ListGroup variant="flush">
<ListGroup className="rounded-0">
{data.map((item) => {
return (
<ListGroupItem className="py-3 px-0" key={item.comment_id}>
<ListGroupItem
className="py-3 px-0 bg-transparent border-start-0 border-end-0"
key={item.comment_id}>
<a
className="text-break"
href={

View File

@ -18,11 +18,11 @@ const Index: FC<Props> = ({ visible, tabName, data }) => {
}
return (
<ListGroup variant="flush">
<ListGroup className="rounded-0">
{data.map((item) => {
return (
<ListGroupItem
className="py-3 px-0"
className="py-3 px-0 bg-transparent border-start-0 border-end-0"
key={tabName === 'questions' ? item.question_id : item.id}>
<h6 className="mb-2">
<a

View File

@ -24,7 +24,7 @@ const Index: FC<Props> = ({
}
return (
<div className="d-flex align-items-center justify-content-between pb-3 border-bottom">
<div className="d-flex align-items-center justify-content-between pb-3">
<h5 className="mb-0">
{count} {t(tabName)}
</h5>

View File

@ -16,10 +16,12 @@ const Index: FC<Props> = ({ visible, data }) => {
return null;
}
return (
<ListGroup variant="flush">
<ListGroup className="rounded-0">
{data.map((item) => {
return (
<ListGroupItem className="d-flex py-3 px-0" key={item.object_id}>
<ListGroupItem
className="d-flex py-3 px-0 bg-transparent border-start-0 border-end-0"
key={item.object_id}>
<div
className={`me-3 text-end ${
item.reputation > 0 ? 'text-success' : 'text-danger'

Some files were not shown because too many files have changed in this diff Show More