Merge branch 'feat/ui-0.5.0' into feat/ui-0.6.0

This commit is contained in:
haitao(lj) 2022-12-01 18:09:47 +08:00
commit 9ceb9e0a07
91 changed files with 3405 additions and 1710 deletions

38
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@ -0,0 +1,38 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]
**Additional context**
Add any other context about the problem here.

View File

@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@ -0,0 +1,7 @@
Fixes #
## Proposed Changes
-
-
-

View File

@ -4,9 +4,9 @@ on:
push:
branches: [ "main" ]
tags:
- 2.*
- 1.*
- 0.*
- v2.*
- v1.*
- v0.*
# pull_request:
# branches: [ "main" ]

View File

@ -27,6 +27,8 @@ stages:
"compile the golang project":
image: golang:1.18
stage: compile-golang
before_script:
- export GOPROXY=https://goproxy.cn,direct
script:
- make generate
- make build

View File

@ -1,6 +1,6 @@
.PHONY: build clean ui
VERSION=0.3.0
VERSION=0.4.0
BIN=answer
DIR_SRC=./cmd/answer
DOCKER_CMD=docker
@ -26,11 +26,10 @@ generate:
go mod tidy
test:
@$(GO) test ./...
@$(GO) test ./internal/repo/repo_test
# clean all build result
clean:
@$(GO) clean ./...
@rm -f $(BIN)

View File

@ -8,10 +8,11 @@ An open-source knowledge-based community software. You can use it to quickly bui
To learn more about the project, visit [answer.dev](https://answer.dev).
[![LICENSE](https://img.shields.io/badge/License-Apache-green)](https://github.com/answerdev/answer/blob/main/LICENSE)
[![Language](https://img.shields.io/badge/Language-Go-blue.svg)](https://golang.org/)
[![Language](https://img.shields.io/badge/Language-React-blue.svg)](https://reactjs.org/)
[![LICENSE](https://img.shields.io/github/license/answerdev/answer)](https://github.com/answerdev/answer/blob/main/LICENSE)
[![Language](https://img.shields.io/badge/language-go-blue.svg)](https://golang.org/)
[![Language](https://img.shields.io/badge/language-react-blue.svg)](https://reactjs.org/)
[![Go Report Card](https://goreportcard.com/badge/github.com/answerdev/answer)](https://goreportcard.com/report/github.com/answerdev/answer)
[![Discord](https://img.shields.io/badge/discord-chat-5865f2?logo=discord&logoColor=f5f5f5)](https://discord.gg/Jm7Y4cbUej)
## Screenshots

View File

@ -4,14 +4,15 @@
# Answer - 构建问答社区
一款极简的、问答形式的知识社区开源软件,用来快速构建产品你的产品问答支持社区、用户问答社区、粉丝社区等。
一款问答形式的知识社区开源软件,用来快速构建产品你的产品技术社区、客户支持社区、用户社区等。
了解更多关于该项目的内容,请访问 [answer.dev](https://answer.dev).
[![LICENSE](https://img.shields.io/badge/License-Apache-green)](https://github.com/answerdev/answer/blob/main/LICENSE)
[![Language](https://img.shields.io/badge/Language-Go-blue.svg)](https://golang.org/)
[![Language](https://img.shields.io/badge/Language-React-blue.svg)](https://reactjs.org/)
[![LICENSE](https://img.shields.io/github/license/answerdev/answer)](https://github.com/answerdev/answer/blob/main/LICENSE)
[![Language](https://img.shields.io/badge/language-go-blue.svg)](https://golang.org/)
[![Language](https://img.shields.io/badge/language-react-blue.svg)](https://reactjs.org/)
[![Go Report Card](https://goreportcard.com/badge/github.com/answerdev/answer)](https://goreportcard.com/report/github.com/answerdev/answer)
[![Discord](https://img.shields.io/badge/discord-chat-5865f2?logo=discord&logoColor=f5f5f5)](https://discord.gg/Jm7Y4cbUej)
## 截图

View File

@ -20,7 +20,7 @@ var (
// Name is the name of the project
Name = "answer"
// Version is the version of the project
Version = "development"
Version = "0.0.0"
// Revision is the git short commit revision number
Revision = ""
// Time is the build time of the project

View File

@ -34,6 +34,7 @@ import (
"github.com/answerdev/answer/internal/repo/search_common"
"github.com/answerdev/answer/internal/repo/site_info"
"github.com/answerdev/answer/internal/repo/tag"
"github.com/answerdev/answer/internal/repo/tag_common"
"github.com/answerdev/answer/internal/repo/unique"
"github.com/answerdev/answer/internal/repo/user"
"github.com/answerdev/answer/internal/router"
@ -58,11 +59,12 @@ import (
"github.com/answerdev/answer/internal/service/report_backyard"
"github.com/answerdev/answer/internal/service/report_handle_backyard"
"github.com/answerdev/answer/internal/service/revision_common"
"github.com/answerdev/answer/internal/service/search_parser"
"github.com/answerdev/answer/internal/service/service_config"
"github.com/answerdev/answer/internal/service/siteinfo"
"github.com/answerdev/answer/internal/service/siteinfo_common"
tag2 "github.com/answerdev/answer/internal/service/tag"
"github.com/answerdev/answer/internal/service/tag_common"
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_common"
@ -115,8 +117,8 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database,
userCommon := usercommon.NewUserCommon(userRepo)
answerRepo := answer.NewAnswerRepo(dataData, uniqueIDRepo, userRankRepo, activityRepo)
questionRepo := question.NewQuestionRepo(dataData, uniqueIDRepo)
tagRepo := tag.NewTagRepo(dataData, uniqueIDRepo)
objService := object_info.NewObjService(answerRepo, questionRepo, commentCommonRepo, tagRepo)
tagCommonRepo := tag_common.NewTagCommonRepo(dataData, uniqueIDRepo)
objService := object_info.NewObjService(answerRepo, questionRepo, commentCommonRepo, tagCommonRepo)
voteRepo := activity_common.NewVoteRepo(dataData, activityRepo)
commentService := comment2.NewCommentService(commentRepo, commentCommonRepo, userCommon, objService, voteRepo)
rankService := rank2.NewRankService(userCommon, userRankRepo, objService, configRepo)
@ -127,18 +129,19 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database,
serviceVoteRepo := activity.NewVoteRepo(dataData, uniqueIDRepo, configRepo, activityRepo, userRankRepo, voteRepo)
voteService := service.NewVoteService(serviceVoteRepo, uniqueIDRepo, configRepo, questionRepo, answerRepo, commentCommonRepo, objService)
voteController := controller.NewVoteController(voteService)
tagRepo := tag.NewTagRepo(dataData, uniqueIDRepo)
tagRelRepo := tag.NewTagRelRepo(dataData)
revisionRepo := revision.NewRevisionRepo(dataData, uniqueIDRepo)
revisionService := revision_common.NewRevisionService(revisionRepo, userRepo)
tagCommonService := tag_common2.NewTagCommonService(tagCommonRepo, tagRelRepo, revisionService, siteInfoCommonService)
followRepo := activity_common.NewFollowRepo(dataData, uniqueIDRepo, activityRepo)
tagService := tag2.NewTagService(tagRepo, revisionService, followRepo)
tagController := controller.NewTagController(tagService, rankService)
tagService := tag2.NewTagService(tagRepo, tagCommonService, revisionService, followRepo, siteInfoCommonService)
tagController := controller.NewTagController(tagService, tagCommonService, rankService)
followFollowRepo := activity.NewFollowRepo(dataData, uniqueIDRepo, activityRepo)
followService := follow.NewFollowService(followFollowRepo, followRepo, tagRepo)
followService := follow.NewFollowService(followFollowRepo, followRepo, tagCommonRepo)
followController := controller.NewFollowController(followService)
collectionRepo := collection.NewCollectionRepo(dataData, uniqueIDRepo)
collectionGroupRepo := collection.NewCollectionGroupRepo(dataData)
tagRelRepo := tag.NewTagRelRepo(dataData)
tagCommonService := tagcommon.NewTagCommonService(tagRepo, tagRelRepo, revisionService)
collectionCommon := collectioncommon.NewCollectionCommon(collectionRepo)
answerCommon := answercommon.NewAnswerCommon(answerRepo)
metaRepo := meta.NewMetaRepo(dataData)
@ -154,8 +157,9 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database,
answerService := service.NewAnswerService(answerRepo, questionRepo, questionCommon, userCommon, collectionCommon, userRepo, revisionService, answerActivityService, answerCommon, voteRepo)
dashboardService := dashboard.NewDashboardService(questionRepo, answerRepo, commentCommonRepo, voteRepo, userRepo, reportRepo, configRepo, siteInfoCommonService, serviceConf, dataData)
answerController := controller.NewAnswerController(answerService, rankService, dashboardService)
searchParser := search_parser.NewSearchParser(tagCommonService, userCommon)
searchRepo := search_common.NewSearchRepo(dataData, uniqueIDRepo, userCommon)
searchService := service.NewSearchService(searchRepo, tagRepo, userCommon, followRepo)
searchService := service.NewSearchService(searchParser, searchRepo)
searchController := controller.NewSearchController(searchService)
serviceRevisionService := service.NewRevisionService(revisionRepo, userCommon, questionCommon, answerService)
revisionController := controller.NewRevisionController(serviceRevisionService)
@ -171,7 +175,7 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database,
reasonService := reason2.NewReasonService(reasonRepo)
reasonController := controller.NewReasonController(reasonService)
themeController := controller_backyard.NewThemeController()
siteInfoService := siteinfo.NewSiteInfoService(siteInfoRepo, emailService)
siteInfoService := siteinfo.NewSiteInfoService(siteInfoRepo, emailService, tagCommonService)
siteInfoController := controller_backyard.NewSiteInfoController(siteInfoService)
siteinfoController := controller.NewSiteinfoController(siteInfoCommonService)
notificationRepo := notification.NewNotificationRepo(dataData)
@ -179,7 +183,8 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database,
notificationService := notification2.NewNotificationService(dataData, notificationRepo, notificationCommon)
notificationController := controller.NewNotificationController(notificationService)
dashboardController := controller.NewDashboardController(dashboardService)
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 := controller.NewUploadController(uploaderService)
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)
swaggerRouter := router.NewSwaggerRouter(swaggerConf)
uiRouter := router.NewUIRouter()
authUserMiddleware := middleware.NewAuthUserMiddleware(authService)

View File

@ -503,6 +503,77 @@ const docTemplate = `{
}
}
},
"/answer/admin/api/siteinfo/branding": {
"get": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "get site interface",
"produces": [
"application/json"
],
"tags": [
"admin"
],
"summary": "get site interface",
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/handler.RespBody"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/schema.SiteBrandingResp"
}
}
}
]
}
}
}
},
"put": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "update site info branding",
"produces": [
"application/json"
],
"tags": [
"admin"
],
"summary": "update site info branding",
"parameters": [
{
"description": "branding info",
"name": "data",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/schema.SiteBrandingReq"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handler.RespBody"
}
}
}
}
},
"/answer/admin/api/siteinfo/general": {
"get": {
"security": [
@ -645,6 +716,148 @@ const docTemplate = `{
}
}
},
"/answer/admin/api/siteinfo/legal": {
"get": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "Set the legal information for the site",
"produces": [
"application/json"
],
"tags": [
"admin"
],
"summary": "Set the legal information for the site",
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/handler.RespBody"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/schema.SiteLegalResp"
}
}
}
]
}
}
}
},
"put": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "update site legal info",
"produces": [
"application/json"
],
"tags": [
"admin"
],
"summary": "update site legal info",
"parameters": [
{
"description": "write info",
"name": "data",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/schema.SiteLegalReq"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handler.RespBody"
}
}
}
}
},
"/answer/admin/api/siteinfo/write": {
"get": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "get site interface",
"produces": [
"application/json"
],
"tags": [
"admin"
],
"summary": "get site interface",
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/handler.RespBody"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/schema.SiteWriteResp"
}
}
}
]
}
}
}
},
"put": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "update site write info",
"produces": [
"application/json"
],
"tags": [
"admin"
],
"summary": "update site write info",
"parameters": [
{
"description": "write info",
"name": "data",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/schema.SiteWriteReq"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handler.RespBody"
}
}
}
}
},
"/answer/admin/api/theme/options": {
"get": {
"security": [
@ -1323,6 +1536,64 @@ const docTemplate = `{
}
}
},
"/answer/api/v1/file": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "upload file",
"consumes": [
"multipart/form-data"
],
"tags": [
"Upload"
],
"summary": "upload file",
"parameters": [
{
"enum": [
"post",
"avatar",
"branding"
],
"type": "string",
"description": "identify the source of the file upload",
"name": "source",
"in": "formData",
"required": true
},
{
"type": "file",
"description": "file",
"name": "file",
"in": "formData",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/handler.RespBody"
},
{
"type": "object",
"properties": {
"data": {
"type": "string"
}
}
}
]
}
}
}
}
},
"/answer/api/v1/follow": {
"post": {
"security": [
@ -2747,6 +3018,51 @@ const docTemplate = `{
}
}
},
"/answer/api/v1/siteinfo/legal": {
"get": {
"description": "get site legal info",
"produces": [
"application/json"
],
"tags": [
"site"
],
"summary": "get site legal info",
"parameters": [
{
"enum": [
"tos",
"privacy"
],
"type": "string",
"description": "legal information type",
"name": "info_type",
"in": "query",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/handler.RespBody"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/schema.GetSiteLegalInfoResp"
}
}
}
]
}
}
}
}
},
"/answer/api/v1/tag": {
"get": {
"description": "get tag one",
@ -3106,52 +3422,6 @@ const docTemplate = `{
}
}
},
"/answer/api/v1/user/avatar/upload": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "UserUpdateInfo",
"consumes": [
"multipart/form-data"
],
"tags": [
"User"
],
"summary": "UserUpdateInfo",
"parameters": [
{
"type": "file",
"description": "file",
"name": "file",
"in": "formData",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/handler.RespBody"
},
{
"type": "object",
"properties": {
"data": {
"type": "string"
}
}
}
]
}
}
}
}
},
"/answer/api/v1/user/email": {
"put": {
"security": [
@ -3671,52 +3941,6 @@ const docTemplate = `{
}
}
},
"/answer/api/v1/user/post/file": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "upload user post file",
"consumes": [
"multipart/form-data"
],
"tags": [
"User"
],
"summary": "upload user post file",
"parameters": [
{
"type": "file",
"description": "file",
"name": "file",
"in": "formData",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/handler.RespBody"
},
{
"type": "object",
"properties": {
"data": {
"type": "string"
}
}
}
]
}
}
}
}
},
"/answer/api/v1/user/register/email": {
"post": {
"description": "UserRegisterByEmail",
@ -4634,6 +4858,12 @@ const docTemplate = `{
"description": "if main tag slug name is not empty, this tag is synonymous with the main tag",
"type": "string"
},
"recommend": {
"type": "boolean"
},
"reserved": {
"type": "boolean"
},
"slug_name": {
"description": "slug name",
"type": "string"
@ -4877,6 +5107,23 @@ const docTemplate = `{
}
}
},
"schema.GetSiteLegalInfoResp": {
"type": "object",
"properties": {
"privacy_policy_original_text": {
"type": "string"
},
"privacy_policy_parsed_text": {
"type": "string"
},
"terms_of_service_original_text": {
"type": "string"
},
"terms_of_service_parsed_text": {
"type": "string"
}
}
},
"schema.GetTagPageResp": {
"type": "object",
"properties": {
@ -4912,6 +5159,12 @@ const docTemplate = `{
"description": "question amount",
"type": "integer"
},
"recommend": {
"type": "boolean"
},
"reserved": {
"type": "boolean"
},
"slug_name": {
"description": "slug_name",
"type": "string"
@ -4972,6 +5225,12 @@ const docTemplate = `{
"description": "question amount",
"type": "integer"
},
"recommend": {
"type": "boolean"
},
"reserved": {
"type": "boolean"
},
"slug_name": {
"description": "slug name",
"type": "string"
@ -5535,6 +5794,9 @@ const docTemplate = `{
"id": {
"type": "string"
},
"question_id": {
"type": "string"
},
"status": {
"description": "Status",
"type": "string"
@ -5571,6 +5833,56 @@ const docTemplate = `{
}
}
},
"schema.SiteBrandingReq": {
"type": "object",
"required": [
"logo",
"square_icon"
],
"properties": {
"favicon": {
"type": "string",
"maxLength": 512
},
"logo": {
"type": "string",
"maxLength": 512
},
"mobile_logo": {
"type": "string",
"maxLength": 512
},
"square_icon": {
"type": "string",
"maxLength": 512
}
}
},
"schema.SiteBrandingResp": {
"type": "object",
"required": [
"logo",
"square_icon"
],
"properties": {
"favicon": {
"type": "string",
"maxLength": 512
},
"logo": {
"type": "string",
"maxLength": 512
},
"mobile_logo": {
"type": "string",
"maxLength": 512
},
"square_icon": {
"type": "string",
"maxLength": 512
}
}
},
"schema.SiteGeneralReq": {
"type": "object",
"required": [
@ -5647,10 +5959,6 @@ const docTemplate = `{
"type": "string",
"maxLength": 128
},
"logo": {
"type": "string",
"maxLength": 256
},
"theme": {
"type": "string",
"maxLength": 128
@ -5673,10 +5981,6 @@ const docTemplate = `{
"type": "string",
"maxLength": 128
},
"logo": {
"type": "string",
"maxLength": 256
},
"theme": {
"type": "string",
"maxLength": 128
@ -5687,6 +5991,80 @@ const docTemplate = `{
}
}
},
"schema.SiteLegalReq": {
"type": "object",
"properties": {
"privacy_policy_original_text": {
"type": "string"
},
"privacy_policy_parsed_text": {
"type": "string"
},
"terms_of_service_original_text": {
"type": "string"
},
"terms_of_service_parsed_text": {
"type": "string"
}
}
},
"schema.SiteLegalResp": {
"type": "object",
"properties": {
"privacy_policy_original_text": {
"type": "string"
},
"privacy_policy_parsed_text": {
"type": "string"
},
"terms_of_service_original_text": {
"type": "string"
},
"terms_of_service_parsed_text": {
"type": "string"
}
}
},
"schema.SiteWriteReq": {
"type": "object",
"properties": {
"recommend_tags": {
"type": "array",
"items": {
"type": "string"
}
},
"required_tag": {
"type": "boolean"
},
"reserved_tags": {
"type": "array",
"items": {
"type": "string"
}
}
}
},
"schema.SiteWriteResp": {
"type": "object",
"properties": {
"recommend_tags": {
"type": "array",
"items": {
"type": "string"
}
},
"required_tag": {
"type": "boolean"
},
"reserved_tags": {
"type": "array",
"items": {
"type": "string"
}
}
}
},
"schema.TagItem": {
"type": "object",
"properties": {
@ -5720,6 +6098,12 @@ const docTemplate = `{
"description": "if main tag slug name is not empty, this tag is synonymous with the main tag",
"type": "string"
},
"recommend": {
"type": "boolean"
},
"reserved": {
"type": "boolean"
},
"slug_name": {
"type": "string"
}
@ -6003,6 +6387,10 @@ const docTemplate = `{
},
"schema.UserEmailLogin": {
"type": "object",
"required": [
"e_mail",
"pass"
],
"properties": {
"captcha_code": {
"description": "captcha_code",
@ -6014,11 +6402,14 @@ const docTemplate = `{
},
"e_mail": {
"description": "e_mail",
"type": "string"
"type": "string",
"maxLength": 500
},
"pass": {
"description": "password",
"type": "string"
"type": "string",
"maxLength": 32,
"minLength": 8
}
}
},

View File

@ -491,6 +491,77 @@
}
}
},
"/answer/admin/api/siteinfo/branding": {
"get": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "get site interface",
"produces": [
"application/json"
],
"tags": [
"admin"
],
"summary": "get site interface",
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/handler.RespBody"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/schema.SiteBrandingResp"
}
}
}
]
}
}
}
},
"put": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "update site info branding",
"produces": [
"application/json"
],
"tags": [
"admin"
],
"summary": "update site info branding",
"parameters": [
{
"description": "branding info",
"name": "data",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/schema.SiteBrandingReq"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handler.RespBody"
}
}
}
}
},
"/answer/admin/api/siteinfo/general": {
"get": {
"security": [
@ -633,6 +704,148 @@
}
}
},
"/answer/admin/api/siteinfo/legal": {
"get": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "Set the legal information for the site",
"produces": [
"application/json"
],
"tags": [
"admin"
],
"summary": "Set the legal information for the site",
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/handler.RespBody"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/schema.SiteLegalResp"
}
}
}
]
}
}
}
},
"put": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "update site legal info",
"produces": [
"application/json"
],
"tags": [
"admin"
],
"summary": "update site legal info",
"parameters": [
{
"description": "write info",
"name": "data",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/schema.SiteLegalReq"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handler.RespBody"
}
}
}
}
},
"/answer/admin/api/siteinfo/write": {
"get": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "get site interface",
"produces": [
"application/json"
],
"tags": [
"admin"
],
"summary": "get site interface",
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/handler.RespBody"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/schema.SiteWriteResp"
}
}
}
]
}
}
}
},
"put": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "update site write info",
"produces": [
"application/json"
],
"tags": [
"admin"
],
"summary": "update site write info",
"parameters": [
{
"description": "write info",
"name": "data",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/schema.SiteWriteReq"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handler.RespBody"
}
}
}
}
},
"/answer/admin/api/theme/options": {
"get": {
"security": [
@ -1311,6 +1524,64 @@
}
}
},
"/answer/api/v1/file": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "upload file",
"consumes": [
"multipart/form-data"
],
"tags": [
"Upload"
],
"summary": "upload file",
"parameters": [
{
"enum": [
"post",
"avatar",
"branding"
],
"type": "string",
"description": "identify the source of the file upload",
"name": "source",
"in": "formData",
"required": true
},
{
"type": "file",
"description": "file",
"name": "file",
"in": "formData",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/handler.RespBody"
},
{
"type": "object",
"properties": {
"data": {
"type": "string"
}
}
}
]
}
}
}
}
},
"/answer/api/v1/follow": {
"post": {
"security": [
@ -2735,6 +3006,51 @@
}
}
},
"/answer/api/v1/siteinfo/legal": {
"get": {
"description": "get site legal info",
"produces": [
"application/json"
],
"tags": [
"site"
],
"summary": "get site legal info",
"parameters": [
{
"enum": [
"tos",
"privacy"
],
"type": "string",
"description": "legal information type",
"name": "info_type",
"in": "query",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/handler.RespBody"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/schema.GetSiteLegalInfoResp"
}
}
}
]
}
}
}
}
},
"/answer/api/v1/tag": {
"get": {
"description": "get tag one",
@ -3094,52 +3410,6 @@
}
}
},
"/answer/api/v1/user/avatar/upload": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "UserUpdateInfo",
"consumes": [
"multipart/form-data"
],
"tags": [
"User"
],
"summary": "UserUpdateInfo",
"parameters": [
{
"type": "file",
"description": "file",
"name": "file",
"in": "formData",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/handler.RespBody"
},
{
"type": "object",
"properties": {
"data": {
"type": "string"
}
}
}
]
}
}
}
}
},
"/answer/api/v1/user/email": {
"put": {
"security": [
@ -3659,52 +3929,6 @@
}
}
},
"/answer/api/v1/user/post/file": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "upload user post file",
"consumes": [
"multipart/form-data"
],
"tags": [
"User"
],
"summary": "upload user post file",
"parameters": [
{
"type": "file",
"description": "file",
"name": "file",
"in": "formData",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/handler.RespBody"
},
{
"type": "object",
"properties": {
"data": {
"type": "string"
}
}
}
]
}
}
}
}
},
"/answer/api/v1/user/register/email": {
"post": {
"description": "UserRegisterByEmail",
@ -4622,6 +4846,12 @@
"description": "if main tag slug name is not empty, this tag is synonymous with the main tag",
"type": "string"
},
"recommend": {
"type": "boolean"
},
"reserved": {
"type": "boolean"
},
"slug_name": {
"description": "slug name",
"type": "string"
@ -4865,6 +5095,23 @@
}
}
},
"schema.GetSiteLegalInfoResp": {
"type": "object",
"properties": {
"privacy_policy_original_text": {
"type": "string"
},
"privacy_policy_parsed_text": {
"type": "string"
},
"terms_of_service_original_text": {
"type": "string"
},
"terms_of_service_parsed_text": {
"type": "string"
}
}
},
"schema.GetTagPageResp": {
"type": "object",
"properties": {
@ -4900,6 +5147,12 @@
"description": "question amount",
"type": "integer"
},
"recommend": {
"type": "boolean"
},
"reserved": {
"type": "boolean"
},
"slug_name": {
"description": "slug_name",
"type": "string"
@ -4960,6 +5213,12 @@
"description": "question amount",
"type": "integer"
},
"recommend": {
"type": "boolean"
},
"reserved": {
"type": "boolean"
},
"slug_name": {
"description": "slug name",
"type": "string"
@ -5523,6 +5782,9 @@
"id": {
"type": "string"
},
"question_id": {
"type": "string"
},
"status": {
"description": "Status",
"type": "string"
@ -5559,6 +5821,56 @@
}
}
},
"schema.SiteBrandingReq": {
"type": "object",
"required": [
"logo",
"square_icon"
],
"properties": {
"favicon": {
"type": "string",
"maxLength": 512
},
"logo": {
"type": "string",
"maxLength": 512
},
"mobile_logo": {
"type": "string",
"maxLength": 512
},
"square_icon": {
"type": "string",
"maxLength": 512
}
}
},
"schema.SiteBrandingResp": {
"type": "object",
"required": [
"logo",
"square_icon"
],
"properties": {
"favicon": {
"type": "string",
"maxLength": 512
},
"logo": {
"type": "string",
"maxLength": 512
},
"mobile_logo": {
"type": "string",
"maxLength": 512
},
"square_icon": {
"type": "string",
"maxLength": 512
}
}
},
"schema.SiteGeneralReq": {
"type": "object",
"required": [
@ -5635,10 +5947,6 @@
"type": "string",
"maxLength": 128
},
"logo": {
"type": "string",
"maxLength": 256
},
"theme": {
"type": "string",
"maxLength": 128
@ -5661,10 +5969,6 @@
"type": "string",
"maxLength": 128
},
"logo": {
"type": "string",
"maxLength": 256
},
"theme": {
"type": "string",
"maxLength": 128
@ -5675,6 +5979,80 @@
}
}
},
"schema.SiteLegalReq": {
"type": "object",
"properties": {
"privacy_policy_original_text": {
"type": "string"
},
"privacy_policy_parsed_text": {
"type": "string"
},
"terms_of_service_original_text": {
"type": "string"
},
"terms_of_service_parsed_text": {
"type": "string"
}
}
},
"schema.SiteLegalResp": {
"type": "object",
"properties": {
"privacy_policy_original_text": {
"type": "string"
},
"privacy_policy_parsed_text": {
"type": "string"
},
"terms_of_service_original_text": {
"type": "string"
},
"terms_of_service_parsed_text": {
"type": "string"
}
}
},
"schema.SiteWriteReq": {
"type": "object",
"properties": {
"recommend_tags": {
"type": "array",
"items": {
"type": "string"
}
},
"required_tag": {
"type": "boolean"
},
"reserved_tags": {
"type": "array",
"items": {
"type": "string"
}
}
}
},
"schema.SiteWriteResp": {
"type": "object",
"properties": {
"recommend_tags": {
"type": "array",
"items": {
"type": "string"
}
},
"required_tag": {
"type": "boolean"
},
"reserved_tags": {
"type": "array",
"items": {
"type": "string"
}
}
}
},
"schema.TagItem": {
"type": "object",
"properties": {
@ -5708,6 +6086,12 @@
"description": "if main tag slug name is not empty, this tag is synonymous with the main tag",
"type": "string"
},
"recommend": {
"type": "boolean"
},
"reserved": {
"type": "boolean"
},
"slug_name": {
"type": "string"
}
@ -5991,6 +6375,10 @@
},
"schema.UserEmailLogin": {
"type": "object",
"required": [
"e_mail",
"pass"
],
"properties": {
"captcha_code": {
"description": "captcha_code",
@ -6002,11 +6390,14 @@
},
"e_mail": {
"description": "e_mail",
"type": "string"
"type": "string",
"maxLength": 500
},
"pass": {
"description": "password",
"type": "string"
"type": "string",
"maxLength": 32,
"minLength": 8
}
}
},

View File

@ -368,6 +368,10 @@ definitions:
description: if main tag slug name is not empty, this tag is synonymous with
the main tag
type: string
recommend:
type: boolean
reserved:
type: boolean
slug_name:
description: slug name
type: string
@ -544,6 +548,17 @@ definitions:
smtp_username:
type: string
type: object
schema.GetSiteLegalInfoResp:
properties:
privacy_policy_original_text:
type: string
privacy_policy_parsed_text:
type: string
terms_of_service_original_text:
type: string
terms_of_service_parsed_text:
type: string
type: object
schema.GetTagPageResp:
properties:
created_at:
@ -570,6 +585,10 @@ definitions:
question_count:
description: question amount
type: integer
recommend:
type: boolean
reserved:
type: boolean
slug_name:
description: slug_name
type: string
@ -615,6 +634,10 @@ definitions:
question_count:
description: question amount
type: integer
recommend:
type: boolean
reserved:
type: boolean
slug_name:
description: slug name
type: string
@ -1025,6 +1048,8 @@ definitions:
type: string
id:
type: string
question_id:
type: string
status:
description: Status
type: string
@ -1050,6 +1075,42 @@ definitions:
description: object_type
type: string
type: object
schema.SiteBrandingReq:
properties:
favicon:
maxLength: 512
type: string
logo:
maxLength: 512
type: string
mobile_logo:
maxLength: 512
type: string
square_icon:
maxLength: 512
type: string
required:
- logo
- square_icon
type: object
schema.SiteBrandingResp:
properties:
favicon:
maxLength: 512
type: string
logo:
maxLength: 512
type: string
mobile_logo:
maxLength: 512
type: string
square_icon:
maxLength: 512
type: string
required:
- logo
- square_icon
type: object
schema.SiteGeneralReq:
properties:
contact_email:
@ -1103,9 +1164,6 @@ definitions:
language:
maxLength: 128
type: string
logo:
maxLength: 256
type: string
theme:
maxLength: 128
type: string
@ -1122,9 +1180,6 @@ definitions:
language:
maxLength: 128
type: string
logo:
maxLength: 256
type: string
theme:
maxLength: 128
type: string
@ -1136,6 +1191,54 @@ definitions:
- theme
- time_zone
type: object
schema.SiteLegalReq:
properties:
privacy_policy_original_text:
type: string
privacy_policy_parsed_text:
type: string
terms_of_service_original_text:
type: string
terms_of_service_parsed_text:
type: string
type: object
schema.SiteLegalResp:
properties:
privacy_policy_original_text:
type: string
privacy_policy_parsed_text:
type: string
terms_of_service_original_text:
type: string
terms_of_service_parsed_text:
type: string
type: object
schema.SiteWriteReq:
properties:
recommend_tags:
items:
type: string
type: array
required_tag:
type: boolean
reserved_tags:
items:
type: string
type: array
type: object
schema.SiteWriteResp:
properties:
recommend_tags:
items:
type: string
type: array
required_tag:
type: boolean
reserved_tags:
items:
type: string
type: array
type: object
schema.TagItem:
properties:
display_name:
@ -1161,6 +1264,10 @@ definitions:
description: if main tag slug name is not empty, this tag is synonymous with
the main tag
type: string
recommend:
type: boolean
reserved:
type: boolean
slug_name:
type: string
type: object
@ -1373,10 +1480,16 @@ definitions:
type: string
e_mail:
description: e_mail
maxLength: 500
type: string
pass:
description: password
maxLength: 32
minLength: 8
type: string
required:
- e_mail
- pass
type: object
schema.UserModifyPassWordRequest:
properties:
@ -1784,6 +1897,47 @@ paths:
summary: update smtp config
tags:
- admin
/answer/admin/api/siteinfo/branding:
get:
description: get site interface
produces:
- application/json
responses:
"200":
description: OK
schema:
allOf:
- $ref: '#/definitions/handler.RespBody'
- properties:
data:
$ref: '#/definitions/schema.SiteBrandingResp'
type: object
security:
- ApiKeyAuth: []
summary: get site interface
tags:
- admin
put:
description: update site info branding
parameters:
- description: branding info
in: body
name: data
required: true
schema:
$ref: '#/definitions/schema.SiteBrandingReq'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handler.RespBody'
security:
- ApiKeyAuth: []
summary: update site info branding
tags:
- admin
/answer/admin/api/siteinfo/general:
get:
description: get site general information
@ -1866,6 +2020,88 @@ paths:
summary: update site info interface
tags:
- admin
/answer/admin/api/siteinfo/legal:
get:
description: Set the legal information for the site
produces:
- application/json
responses:
"200":
description: OK
schema:
allOf:
- $ref: '#/definitions/handler.RespBody'
- properties:
data:
$ref: '#/definitions/schema.SiteLegalResp'
type: object
security:
- ApiKeyAuth: []
summary: Set the legal information for the site
tags:
- admin
put:
description: update site legal info
parameters:
- description: write info
in: body
name: data
required: true
schema:
$ref: '#/definitions/schema.SiteLegalReq'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handler.RespBody'
security:
- ApiKeyAuth: []
summary: update site legal info
tags:
- admin
/answer/admin/api/siteinfo/write:
get:
description: get site interface
produces:
- application/json
responses:
"200":
description: OK
schema:
allOf:
- $ref: '#/definitions/handler.RespBody'
- properties:
data:
$ref: '#/definitions/schema.SiteWriteResp'
type: object
security:
- ApiKeyAuth: []
summary: get site interface
tags:
- admin
put:
description: update site write info
parameters:
- description: write info
in: body
name: data
required: true
schema:
$ref: '#/definitions/schema.SiteWriteReq'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handler.RespBody'
security:
- ApiKeyAuth: []
summary: update site write info
tags:
- admin
/answer/admin/api/theme/options:
get:
description: Get theme options
@ -2272,6 +2508,41 @@ paths:
summary: get comment page
tags:
- Comment
/answer/api/v1/file:
post:
consumes:
- multipart/form-data
description: upload file
parameters:
- description: identify the source of the file upload
enum:
- post
- avatar
- branding
in: formData
name: source
required: true
type: string
- description: file
in: formData
name: file
required: true
type: file
responses:
"200":
description: OK
schema:
allOf:
- $ref: '#/definitions/handler.RespBody'
- properties:
data:
type: string
type: object
security:
- ApiKeyAuth: []
summary: upload file
tags:
- Upload
/answer/api/v1/follow:
post:
consumes:
@ -3145,6 +3416,33 @@ paths:
summary: get site info
tags:
- site
/answer/api/v1/siteinfo/legal:
get:
description: get site legal info
parameters:
- description: legal information type
enum:
- tos
- privacy
in: query
name: info_type
required: true
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
allOf:
- $ref: '#/definitions/handler.RespBody'
- properties:
data:
$ref: '#/definitions/schema.GetSiteLegalInfoResp'
type: object
summary: get site legal info
tags:
- site
/answer/api/v1/tag:
delete:
consumes:
@ -3362,32 +3660,6 @@ paths:
summary: ActionRecord
tags:
- User
/answer/api/v1/user/avatar/upload:
post:
consumes:
- multipart/form-data
description: UserUpdateInfo
parameters:
- description: file
in: formData
name: file
required: true
type: file
responses:
"200":
description: OK
schema:
allOf:
- $ref: '#/definitions/handler.RespBody'
- properties:
data:
type: string
type: object
security:
- ApiKeyAuth: []
summary: UserUpdateInfo
tags:
- User
/answer/api/v1/user/email:
put:
consumes:
@ -3708,32 +3980,6 @@ paths:
summary: RetrievePassWord
tags:
- User
/answer/api/v1/user/post/file:
post:
consumes:
- multipart/form-data
description: upload user post file
parameters:
- description: file
in: formData
name: file
required: true
type: file
responses:
"200":
description: OK
schema:
allOf:
- $ref: '#/definitions/handler.RespBody'
- properties:
data:
type: string
type: object
security:
- ApiKeyAuth: []
summary: upload user post file
tags:
- User
/answer/api/v1/user/register/email:
post:
consumes:

View File

@ -74,6 +74,10 @@ backend:
tag:
not_found:
other: "Tag not found."
recommend_tag_not_found:
other: "Recommend Tag is not exist."
not_contain_synonym_tags:
other: "Should not contain synonym tags."
theme:
not_found:
other: "Theme not found."
@ -1187,8 +1191,9 @@ ui:
label: Recommend Tags
text: "Please input tag slug above, one tag per line."
required_tag:
label: Required Tag
text: "Every new question must have at least one recommend 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."
@ -1198,7 +1203,7 @@ ui:
btn_submit: Save
not_found_props: "Required property {{ key }} not found."
page_review:
review: Reivew
review: Review
proposed: proposed
question_edit: Question edit
answer_edit: Answer edit
@ -1224,11 +1229,12 @@ ui:
reopened: reopened
created: created
title: "History for"
tag_title: "Timeline for"
show_votes: "Show votes"
n_or_a: N/A
title_for_question: "Timeline for"
title_for_answer: "Timeline for answers to {{ title }} by {{ author }}"
title_for_tag: "Title for tag"
title_for_answer: "Timeline for answer to {{ title }} by {{ author }}"
title_for_tag: "Timeline for tag"
datetime: Datetime
type: Type
by: By

View File

@ -571,7 +571,7 @@ ui:
character: '用户名只能由 "a-z", "0-9", " - . _" 组成'
avatar:
label: 头像
text: 您可以上传图片作为头像,也可以 <1>重置</1> 为
text: 您可以上传图片作为头像
bio:
label: 关于我 (可选)
website:

View File

@ -53,4 +53,8 @@ var (
const (
SiteTypeGeneral = "general"
SiteTypeInterface = "interface"
SiteTypeBranding = "branding"
SiteTypeWrite = "write"
SiteTypeLegal = "legal"
)

View File

@ -14,7 +14,7 @@ const (
)
const (
EmailOrPasswordWrong = "error.user.email_or_password_wrong"
EmailOrPasswordWrong = "error.object.email_or_password_incorrect"
CommentNotFound = "error.comment.not_found"
QuestionNotFound = "error.question.not_found"
AnswerNotFound = "error.answer.not_found"
@ -23,6 +23,8 @@ const (
DisallowFollow = "error.object.disallow_follow"
DisallowVoteYourSelf = "error.object.disallow_vote_your_self"
CaptchaVerificationFailed = "error.object.captcha_verification_failed"
OldPasswordVerificationFailed = "error.object.old_password_verification_failed"
NewPasswordSameAsPreviousSetting = "error.object.new_password_same_as_previous_setting"
UserNotFound = "error.user.not_found"
UsernameInvalid = "error.user.username_invalid"
UsernameDuplicate = "error.user.username_duplicate"
@ -33,6 +35,7 @@ const (
UserSuspended = "error.user.suspended"
ObjectNotFound = "error.object.not_found"
TagNotFound = "error.tag.not_found"
TagNotContainSynonym = "error.tag.not_contain_synonym_tags"
RankFailToMeetTheCondition = "error.rank.fail_to_meet_the_condition"
ThemeNotFound = "error.theme.not_found"
LangNotFound = "error.lang.not_found"
@ -43,4 +46,6 @@ const (
InstallCreateTableFailed = "error.database.create_table_failed"
InstallConfigFailed = "error.install.create_config_failed"
SiteInfoNotFound = "error.site_info.not_found"
UploadFileSourceUnsupported = "error.upload.source_unsupported"
RecommendTagNotExist = "error.tag.recommend_tag_not_found"
)

View File

@ -26,10 +26,10 @@ type MyValidator struct {
Lang i18n.Language
}
// ErrorField error field
type ErrorField struct {
Key string `json:"key"`
Value string `json:"value"`
// FormErrorField indicates the current form error content. which field is error and error message.
type FormErrorField struct {
ErrorField string `json:"error_field"`
ErrorMsg string `json:"error_msg"`
}
// GlobalValidatorMapping is a mapping from validator to translator used
@ -87,7 +87,7 @@ func GetValidatorByLang(la string) *MyValidator {
}
// Check /
func (m *MyValidator) Check(value interface{}) (errField *ErrorField, err error) {
func (m *MyValidator) Check(value interface{}) (errFields []*FormErrorField, err error) {
err = m.Validate.Struct(value)
if err != nil {
var valErrors validator.ValidationErrors
@ -97,9 +97,9 @@ func (m *MyValidator) Check(value interface{}) (errField *ErrorField, err error)
}
for _, fieldError := range valErrors {
errField = &ErrorField{
Key: fieldError.Field(),
Value: fieldError.Translate(m.Tran),
errField := &FormErrorField{
ErrorField: fieldError.Field(),
ErrorMsg: fieldError.Translate(m.Tran),
}
// get original tag name from value for set err field key.
@ -108,17 +108,24 @@ func (m *MyValidator) Check(value interface{}) (errField *ErrorField, err error)
if found {
originalTag := getObjectTagByFieldName(value, fieldName)
if len(originalTag) > 0 {
errField.Key = originalTag
errField.ErrorField = originalTag
}
}
return errField, myErrors.BadRequest(reason.RequestFormatError).WithMsg(fieldError.Translate(m.Tran))
errFields = append(errFields, errField)
}
if len(errFields) > 0 {
errMsg := ""
if len(errFields) == 1 {
errMsg = errFields[0].ErrorMsg
}
return errFields, myErrors.BadRequest(reason.RequestFormatError).WithMsg(errMsg)
}
}
if v, ok := value.(Checker); ok {
errField, err = v.Check()
errFields, err = v.Check()
if err != nil {
return errField, err
return errFields, err
}
}
return nil, nil
@ -126,7 +133,7 @@ func (m *MyValidator) Check(value interface{}) (errField *ErrorField, err error)
// Checker .
type Checker interface {
Check() (errField *ErrorField, err error)
Check() (errField []*FormErrorField, err error)
}
func getObjectTagByFieldName(obj interface{}, fieldName string) (tag string) {

View File

@ -21,4 +21,5 @@ var ProviderSetController = wire.NewSet(
NewNotificationController,
NewSiteinfoController,
NewDashboardController,
NewUploadController,
)

View File

@ -5,6 +5,7 @@ import (
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/siteinfo_common"
"github.com/gin-gonic/gin"
"github.com/segmentfault/pacman/log"
)
type SiteinfoController struct {
@ -30,9 +31,45 @@ func (sc *SiteinfoController) GetSiteInfo(ctx *gin.Context) {
resp := &schema.SiteInfoResp{}
resp.General, err = sc.siteInfoService.GetSiteGeneral(ctx)
if err != nil {
handler.HandleResponse(ctx, err, resp)
log.Error(err)
}
resp.Interface, err = sc.siteInfoService.GetSiteInterface(ctx)
if err != nil {
log.Error(err)
}
resp.Branding, err = sc.siteInfoService.GetSiteBranding(ctx)
if err != nil {
log.Error(err)
}
handler.HandleResponse(ctx, nil, resp)
}
// GetSiteLegalInfo get site legal info
// @Summary get site legal info
// @Description get site legal info
// @Tags site
// @Param info_type query string true "legal information type" Enums(tos, privacy)
// @Produce json
// @Success 200 {object} handler.RespBody{data=schema.GetSiteLegalInfoResp}
// @Router /answer/api/v1/siteinfo/legal [get]
func (sc *SiteinfoController) GetSiteLegalInfo(ctx *gin.Context) {
req := &schema.GetSiteLegalInfoReq{}
if handler.BindAndCheck(ctx, req) {
return
}
resp.Face, err = sc.siteInfoService.GetSiteInterface(ctx)
handler.HandleResponse(ctx, err, resp)
siteLegal, err := sc.siteInfoService.GetSiteLegal(ctx)
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
}
resp := &schema.GetSiteLegalInfoResp{}
if req.IsTOS() {
resp.TermsOfServiceOriginalText = siteLegal.TermsOfServiceOriginalText
resp.TermsOfServiceParsedText = siteLegal.TermsOfServiceParsedText
} else if req.IsPrivacy() {
resp.PrivacyPolicyOriginalText = siteLegal.PrivacyPolicyOriginalText
resp.PrivacyPolicyParsedText = siteLegal.PrivacyPolicyParsedText
}
handler.HandleResponse(ctx, nil, resp)
}

View File

@ -7,19 +7,25 @@ import (
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/rank"
"github.com/answerdev/answer/internal/service/tag"
"github.com/answerdev/answer/internal/service/tag_common"
"github.com/gin-gonic/gin"
"github.com/segmentfault/pacman/errors"
)
// TagController tag controller
type TagController struct {
tagService *tag.TagService
rankService *rank.RankService
tagService *tag.TagService
tagCommonService *tag_common.TagCommonService
rankService *rank.RankService
}
// NewTagController new controller
func NewTagController(tagService *tag.TagService, rankService *rank.RankService) *TagController {
return &TagController{tagService: tagService, rankService: rankService}
func NewTagController(
tagService *tag.TagService,
tagCommonService *tag_common.TagCommonService,
rankService *rank.RankService,
) *TagController {
return &TagController{tagService: tagService, tagCommonService: tagCommonService, rankService: rankService}
}
// SearchTagLike get tag list
@ -36,8 +42,9 @@ func (tc *TagController) SearchTagLike(ctx *gin.Context) {
if handler.BindAndCheck(ctx, req) {
return
}
resp, err := tc.tagService.SearchTagLike(ctx, req)
userinfo := middleware.GetUserInfoFromContext(ctx)
req.IsAdmin = userinfo.IsAdmin
resp, err := tc.tagCommonService.SearchTagLike(ctx, req)
handler.HandleResponse(ctx, err, resp)
}

View File

@ -0,0 +1,65 @@
package controller
import (
"github.com/answerdev/answer/internal/base/handler"
"github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/internal/service/uploader"
"github.com/gin-gonic/gin"
"github.com/segmentfault/pacman/errors"
)
const (
// file is uploaded by markdown(or something else) editor
fileFromPost = "post"
// file is used to change the user's avatar
fileFromAvatar = "avatar"
// file is logo/icon images
fileFromBranding = "branding"
)
// UploadController upload controller
type UploadController struct {
uploaderService *uploader.UploaderService
}
// NewUploadController new controller
func NewUploadController(uploaderService *uploader.UploaderService) *UploadController {
return &UploadController{
uploaderService: uploaderService,
}
}
// UploadFile upload file
// @Summary upload file
// @Description upload file
// @Tags Upload
// @Accept multipart/form-data
// @Security ApiKeyAuth
// @Param source formData string true "identify the source of the file upload" Enums(post, avatar, branding)
// @Param file formData file true "file"
// @Success 200 {object} handler.RespBody{data=string}
// @Router /answer/api/v1/file [post]
func (uc *UploadController) UploadFile(ctx *gin.Context) {
var (
url string
err error
)
source := ctx.PostForm("source")
switch source {
case fileFromAvatar:
url, err = uc.uploaderService.UploadAvatarFile(ctx)
case fileFromPost:
url, err = uc.uploaderService.UploadPostFile(ctx)
case fileFromBranding:
url, err = uc.uploaderService.UploadBrandingFile(ctx)
default:
handler.HandleResponse(ctx, errors.BadRequest(reason.UploadFileSourceUnsupported), nil)
return
}
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
}
handler.HandleResponse(ctx, err, url)
}

View File

@ -1,14 +1,11 @@
package controller
import (
"net/http"
"path"
"strings"
"github.com/answerdev/answer/internal/base/handler"
"github.com/answerdev/answer/internal/base/middleware"
"github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/internal/base/translator"
"github.com/answerdev/answer/internal/base/validator"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service"
"github.com/answerdev/answer/internal/service/action"
@ -17,7 +14,6 @@ import (
"github.com/answerdev/answer/internal/service/uploader"
"github.com/gin-gonic/gin"
"github.com/segmentfault/pacman/errors"
"github.com/segmentfault/pacman/log"
)
// UserController user controller
@ -106,24 +102,22 @@ func (uc *UserController) UserEmailLogin(ctx *gin.Context) {
captchaPass := uc.actionService.ActionRecordVerifyCaptcha(ctx, schema.ActionRecordTypeLogin, ctx.ClientIP(), req.CaptchaID, req.CaptchaCode)
if !captchaPass {
resp := schema.UserVerifyEmailErrorResponse{
Key: "captcha_code",
Value: "error.object.verification_failed",
}
resp.Value = translator.GlobalTrans.Tr(handler.GetLang(ctx), resp.Value)
handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), resp)
errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{
ErrorField: "captcha_code",
ErrorMsg: translator.GlobalTrans.Tr(handler.GetLang(ctx), reason.CaptchaVerificationFailed),
})
handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields)
return
}
resp, err := uc.userService.EmailLogin(ctx, req)
if err != nil {
_, _ = uc.actionService.ActionRecordAdd(ctx, schema.ActionRecordTypeLogin, ctx.ClientIP())
resp := schema.UserVerifyEmailErrorResponse{
Key: "e_mail",
Value: "error.object.email_or_password_incorrect",
}
resp.Value = translator.GlobalTrans.Tr(handler.GetLang(ctx), resp.Value)
handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), resp)
errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{
ErrorField: "e_mail",
ErrorMsg: translator.GlobalTrans.Tr(handler.GetLang(ctx), reason.EmailOrPasswordWrong),
})
handler.HandleResponse(ctx, errors.BadRequest(reason.EmailOrPasswordWrong), errFields)
return
}
uc.actionService.ActionRecordDel(ctx, schema.ActionRecordTypeLogin, ctx.ClientIP())
@ -146,12 +140,11 @@ func (uc *UserController) RetrievePassWord(ctx *gin.Context) {
}
captchaPass := uc.actionService.ActionRecordVerifyCaptcha(ctx, schema.ActionRecordTypeFindPass, ctx.ClientIP(), req.CaptchaID, req.CaptchaCode)
if !captchaPass {
resp := schema.UserVerifyEmailErrorResponse{
Key: "captcha_code",
Value: "error.object.verification_failed",
}
resp.Value = translator.GlobalTrans.Tr(handler.GetLang(ctx), resp.Value)
handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), resp)
errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{
ErrorField: "captcha_code",
ErrorMsg: translator.GlobalTrans.Tr(handler.GetLang(ctx), reason.CaptchaVerificationFailed),
})
handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields)
return
}
_, _ = uc.actionService.ActionRecordAdd(ctx, schema.ActionRecordTypeFindPass, ctx.ClientIP())
@ -277,12 +270,11 @@ func (uc *UserController) UserVerifyEmailSend(ctx *gin.Context) {
captchaPass := uc.actionService.ActionRecordVerifyCaptcha(ctx, schema.ActionRecordTypeEmail, ctx.ClientIP(),
req.CaptchaID, req.CaptchaCode)
if !captchaPass {
resp := schema.UserVerifyEmailErrorResponse{
Key: "captcha_code",
Value: "error.object.verification_failed",
}
resp.Value = translator.GlobalTrans.Tr(handler.GetLang(ctx), resp.Value)
handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), resp)
errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{
ErrorField: "captcha_code",
ErrorMsg: translator.GlobalTrans.Tr(handler.GetLang(ctx), reason.CaptchaVerificationFailed),
})
handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields)
return
}
@ -314,22 +306,19 @@ func (uc *UserController) UserModifyPassWord(ctx *gin.Context) {
return
}
if !oldPassVerification {
resp := schema.UserVerifyEmailErrorResponse{
Key: "old_pass",
Value: "error.object.old_password_verification_failed",
}
resp.Value = translator.GlobalTrans.Tr(handler.GetLang(ctx), resp.Value)
handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), resp)
errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{
ErrorField: "old_pass",
ErrorMsg: translator.GlobalTrans.Tr(handler.GetLang(ctx), reason.OldPasswordVerificationFailed),
})
handler.HandleResponse(ctx, errors.BadRequest(reason.OldPasswordVerificationFailed), errFields)
return
}
if req.OldPass == req.Pass {
resp := schema.UserVerifyEmailErrorResponse{
Key: "pass",
Value: "error.object.new_password_same_as_previous_setting",
}
resp.Value = translator.GlobalTrans.Tr(handler.GetLang(ctx), resp.Value)
handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), resp)
errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{
ErrorField: "pass",
ErrorMsg: translator.GlobalTrans.Tr(handler.GetLang(ctx), reason.NewPasswordSameAsPreviousSetting),
})
handler.HandleResponse(ctx, errors.BadRequest(reason.NewPasswordSameAsPreviousSetting), errFields)
return
}
err = uc.userService.UserModifyPassword(ctx, req)
@ -378,66 +367,6 @@ func (uc *UserController) UserUpdateInterface(ctx *gin.Context) {
handler.HandleResponse(ctx, err, nil)
}
// UploadUserAvatar godoc
// @Summary UserUpdateInfo
// @Description UserUpdateInfo
// @Tags User
// @Accept multipart/form-data
// @Security ApiKeyAuth
// @Param file formData file true "file"
// @Success 200 {object} handler.RespBody{data=string}
// @Router /answer/api/v1/user/avatar/upload [post]
func (uc *UserController) UploadUserAvatar(ctx *gin.Context) {
// max size
var filesMax int64 = 5 << 20
var valuesMax int64 = 5
ctx.Request.Body = http.MaxBytesReader(ctx.Writer, ctx.Request.Body, filesMax+valuesMax)
_, header, err := ctx.Request.FormFile("file")
if err != nil {
log.Error(err.Error())
handler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), nil)
return
}
fileExt := strings.ToLower(path.Ext(header.Filename))
if fileExt != ".jpg" && fileExt != ".png" && fileExt != ".jpeg" {
log.Errorf("upload file format is not supported: %s", fileExt)
handler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), nil)
return
}
url, err := uc.uploaderService.UploadAvatarFile(ctx, header, fileExt)
handler.HandleResponse(ctx, err, url)
}
// UploadUserPostFile godoc
// @Summary upload user post file
// @Description upload user post file
// @Tags User
// @Accept multipart/form-data
// @Security ApiKeyAuth
// @Param file formData file true "file"
// @Success 200 {object} handler.RespBody{data=string}
// @Router /answer/api/v1/user/post/file [post]
func (uc *UserController) UploadUserPostFile(ctx *gin.Context) {
// max size
ctx.Request.Body = http.MaxBytesReader(ctx.Writer, ctx.Request.Body, 10*1024*1024)
_, header, err := ctx.Request.FormFile("file")
if err != nil {
log.Error(err.Error())
handler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), nil)
return
}
fileExt := strings.ToLower(path.Ext(header.Filename))
if fileExt != ".jpg" && fileExt != ".png" && fileExt != ".jpeg" {
log.Errorf("upload file format is not supported: %s", fileExt)
handler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), nil)
return
}
url, err := uc.uploaderService.UploadPostFile(ctx, header, fileExt)
handler.HandleResponse(ctx, err, url)
}
// ActionRecord godoc
// @Summary ActionRecord
// @Description ActionRecord
@ -502,19 +431,18 @@ func (uc *UserController) UserChangeEmailSendCode(ctx *gin.Context) {
captchaPass := uc.actionService.ActionRecordVerifyCaptcha(ctx, schema.ActionRecordTypeEmail, ctx.ClientIP(), req.CaptchaID, req.CaptchaCode)
if !captchaPass {
resp := schema.UserVerifyEmailErrorResponse{
Key: "captcha_code",
Value: "error.object.verification_failed",
}
resp.Value = translator.GlobalTrans.Tr(handler.GetLang(ctx), resp.Value)
handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), resp)
errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{
ErrorField: "captcha_code",
ErrorMsg: translator.GlobalTrans.Tr(handler.GetLang(ctx), reason.CaptchaVerificationFailed),
})
handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields)
return
}
_, _ = uc.actionService.ActionRecordAdd(ctx, schema.ActionRecordTypeEmail, ctx.ClientIP())
resp, err := uc.userService.UserChangeEmailSendCode(ctx, req)
if err != nil {
if resp != nil {
resp.Value = translator.GlobalTrans.Tr(handler.GetLang(ctx), resp.Value)
resp.ErrorMsg = translator.GlobalTrans.Tr(handler.GetLang(ctx), resp.ErrorMsg)
}
handler.HandleResponse(ctx, err, resp)
return

View File

@ -2,16 +2,18 @@ package controller_backyard
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/siteinfo"
"github.com/gin-gonic/gin"
)
// SiteInfoController site info controller
type SiteInfoController struct {
siteInfoService *siteinfo.SiteInfoService
}
// NewSiteInfoController new siteinfo controller.
// NewSiteInfoController new site info controller
func NewSiteInfoController(siteInfoService *siteinfo.SiteInfoService) *SiteInfoController {
return &SiteInfoController{
siteInfoService: siteInfoService,
@ -44,6 +46,45 @@ func (sc *SiteInfoController) GetInterface(ctx *gin.Context) {
handler.HandleResponse(ctx, err, resp)
}
// GetSiteBranding get site interface
// @Summary get site interface
// @Description get site interface
// @Security ApiKeyAuth
// @Tags admin
// @Produce json
// @Success 200 {object} handler.RespBody{data=schema.SiteBrandingResp}
// @Router /answer/admin/api/siteinfo/branding [get]
func (sc *SiteInfoController) GetSiteBranding(ctx *gin.Context) {
resp, err := sc.siteInfoService.GetSiteBranding(ctx)
handler.HandleResponse(ctx, err, resp)
}
// GetSiteWrite get site interface
// @Summary get site interface
// @Description get site interface
// @Security ApiKeyAuth
// @Tags admin
// @Produce json
// @Success 200 {object} handler.RespBody{data=schema.SiteWriteResp}
// @Router /answer/admin/api/siteinfo/write [get]
func (sc *SiteInfoController) GetSiteWrite(ctx *gin.Context) {
resp, err := sc.siteInfoService.GetSiteWrite(ctx)
handler.HandleResponse(ctx, err, resp)
}
// GetSiteLegal Set the legal information for the site
// @Summary Set the legal information for the site
// @Description Set the legal information for the site
// @Security ApiKeyAuth
// @Tags admin
// @Produce json
// @Success 200 {object} handler.RespBody{data=schema.SiteLegalResp}
// @Router /answer/admin/api/siteinfo/legal [get]
func (sc *SiteInfoController) GetSiteLegal(ctx *gin.Context) {
resp, err := sc.siteInfoService.GetSiteLegal(ctx)
handler.HandleResponse(ctx, err, resp)
}
// UpdateGeneral update site general information
// @Summary update site general information
// @Description update site general information
@ -80,6 +121,62 @@ func (sc *SiteInfoController) UpdateInterface(ctx *gin.Context) {
handler.HandleResponse(ctx, err, nil)
}
// UpdateBranding update site branding
// @Summary update site info branding
// @Description update site info branding
// @Security ApiKeyAuth
// @Tags admin
// @Produce json
// @Param data body schema.SiteBrandingReq true "branding info"
// @Success 200 {object} handler.RespBody{}
// @Router /answer/admin/api/siteinfo/branding [put]
func (sc *SiteInfoController) UpdateBranding(ctx *gin.Context) {
req := &schema.SiteBrandingReq{}
if handler.BindAndCheck(ctx, req) {
return
}
err := sc.siteInfoService.SaveSiteBranding(ctx, req)
handler.HandleResponse(ctx, err, nil)
}
// UpdateSiteWrite update site write info
// @Summary update site write info
// @Description update site write info
// @Security ApiKeyAuth
// @Tags admin
// @Produce json
// @Param data body schema.SiteWriteReq true "write info"
// @Success 200 {object} handler.RespBody{}
// @Router /answer/admin/api/siteinfo/write [put]
func (sc *SiteInfoController) UpdateSiteWrite(ctx *gin.Context) {
req := &schema.SiteWriteReq{}
if handler.BindAndCheck(ctx, req) {
return
}
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
resp, err := sc.siteInfoService.SaveSiteWrite(ctx, req)
handler.HandleResponse(ctx, err, resp)
}
// UpdateSiteLegal update site legal info
// @Summary update site legal info
// @Description update site legal info
// @Security ApiKeyAuth
// @Tags admin
// @Produce json
// @Param data body schema.SiteLegalReq true "write info"
// @Success 200 {object} handler.RespBody{}
// @Router /answer/admin/api/siteinfo/legal [put]
func (sc *SiteInfoController) UpdateSiteLegal(ctx *gin.Context) {
req := &schema.SiteLegalReq{}
if handler.BindAndCheck(ctx, req) {
return
}
err := sc.siteInfoService.SaveSiteLegal(ctx, req)
handler.HandleResponse(ctx, err, nil)
}
// GetSMTPConfig get smtp config
// @Summary GetSMTPConfig get smtp config
// @Description GetSMTPConfig get smtp config

View File

@ -5,4 +5,5 @@ type UserCacheInfo struct {
UserID string `json:"user_id"`
UserStatus int `json:"user_status"`
EmailStatus int `json:"email_status"`
IsAdmin bool `json:"is_admin"`
}

View File

@ -21,6 +21,8 @@ type Tag struct {
FollowCount int `xorm:"not null default 0 INT(11) follow_count"`
QuestionCount int `xorm:"not null default 0 INT(11) question_count"`
Status int `xorm:"not null default 1 INT(11) status"`
Recommend bool `xorm:"not null default false BOOL recommend"`
Reserved bool `xorm:"not null default false BOOL reserved"`
RevisionID string `xorm:"not null default 0 BIGINT(20) revision_id"`
}

View File

@ -43,6 +43,7 @@ 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),
}
// GetCurrentDBVersion returns the current db version

13
internal/migrations/v2.go Normal file
View File

@ -0,0 +1,13 @@
package migrations
import (
"xorm.io/xorm"
)
func addTagRecommendedAndReserved(x *xorm.Engine) error {
type Tag struct {
Recommend bool `xorm:"not null default false BOOL recommend"`
Reserved bool `xorm:"not null default false BOOL reserved"`
}
return x.Sync(new(Tag))
}

View File

@ -204,7 +204,7 @@ func (ar *answerRepo) SearchList(ctx context.Context, search *entity.AnswerSearc
case entity.AnswerSearchOrderByVote:
session = session.OrderBy("vote_count desc")
default:
session = session.OrderBy("adopted desc,vote_count desc")
session = session.OrderBy("adopted desc,vote_count desc,created_at asc")
}
session = session.And("status = ?", entity.AnswerStatusAvailable)

View File

@ -22,6 +22,7 @@ import (
"github.com/answerdev/answer/internal/repo/search_common"
"github.com/answerdev/answer/internal/repo/site_info"
"github.com/answerdev/answer/internal/repo/tag"
"github.com/answerdev/answer/internal/repo/tag_common"
"github.com/answerdev/answer/internal/repo/unique"
"github.com/answerdev/answer/internal/repo/user"
"github.com/google/wire"
@ -53,6 +54,7 @@ var ProviderSetRepo = wire.NewSet(
activity.NewQuestionActivityRepo,
activity.NewUserActiveActivityRepo,
tag.NewTagRepo,
tag_common.NewTagCommonRepo,
tag.NewTagRelRepo,
collection.NewCollectionRepo,
collection.NewCollectionGroupRepo,

View File

@ -81,7 +81,7 @@ func Test_notificationRepo_GetNotificationPage(t *testing.T) {
err := notificationRepo.AddNotification(context.TODO(), ent)
assert.NoError(t, err)
notificationPage, total, err := notificationRepo.GetNotificationPage(context.TODO(), &schema.NotificationSearch{UserID: userID})
notificationPage, total, err := notificationRepo.GetNotificationPage(context.TODO(), &schema.NotificationSearch{UserID: ent.UserID})
assert.NoError(t, err)
assert.True(t, total > 0)
assert.Equal(t, notificationPage[0].UserID, ent.UserID)

View File

@ -52,7 +52,8 @@ var (
func TestMain(t *testing.M) {
dbSetting, ok := dbSettingMapping[os.Getenv("TEST_DB_DRIVER")]
if !ok {
dbSetting = dbSettingMapping[string(schemas.MYSQL)]
// Use sqlite3 to test.
dbSetting = dbSettingMapping[string(schemas.SQLITE)]
}
defer func() {
if tearDown != nil {

View File

@ -8,6 +8,7 @@ import (
"github.com/answerdev/answer/internal/entity"
"github.com/answerdev/answer/internal/repo/tag"
"github.com/answerdev/answer/internal/repo/tag_common"
"github.com/answerdev/answer/internal/repo/unique"
"github.com/answerdev/answer/pkg/converter"
"github.com/stretchr/testify/assert"
@ -42,8 +43,8 @@ var (
func addTagList() {
uniqueIDRepo := unique.NewUniqueIDRepo(testDataSource)
tagRepo := tag.NewTagRepo(testDataSource, uniqueIDRepo)
err := tagRepo.AddTagList(context.TODO(), testTagList)
tagCommonRepo := tag_common.NewTagCommonRepo(testDataSource, uniqueIDRepo)
err := tagCommonRepo.AddTagList(context.TODO(), testTagList)
if err != nil {
panic(err)
}
@ -51,9 +52,9 @@ func addTagList() {
func Test_tagRepo_GetTagByID(t *testing.T) {
tagOnce.Do(addTagList)
tagRepo := tag.NewTagRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource))
tagCommonRepo := tag_common.NewTagCommonRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource))
gotTag, exist, err := tagRepo.GetTagByID(context.TODO(), testTagList[0].ID)
gotTag, exist, err := tagCommonRepo.GetTagByID(context.TODO(), testTagList[0].ID)
assert.NoError(t, err)
assert.True(t, exist)
assert.Equal(t, testTagList[0].SlugName, gotTag.SlugName)
@ -61,9 +62,9 @@ func Test_tagRepo_GetTagByID(t *testing.T) {
func Test_tagRepo_GetTagBySlugName(t *testing.T) {
tagOnce.Do(addTagList)
tagRepo := tag.NewTagRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource))
tagCommonRepo := tag_common.NewTagCommonRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource))
gotTag, exist, err := tagRepo.GetTagBySlugName(context.TODO(), testTagList[0].SlugName)
gotTag, exist, err := tagCommonRepo.GetTagBySlugName(context.TODO(), testTagList[0].SlugName)
assert.NoError(t, err)
assert.True(t, exist)
assert.Equal(t, testTagList[0].SlugName, gotTag.SlugName)
@ -80,36 +81,36 @@ func Test_tagRepo_GetTagList(t *testing.T) {
func Test_tagRepo_GetTagListByIDs(t *testing.T) {
tagOnce.Do(addTagList)
tagRepo := tag.NewTagRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource))
tagCommonRepo := tag_common.NewTagCommonRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource))
gotTags, err := tagRepo.GetTagListByIDs(context.TODO(), []string{testTagList[0].ID})
gotTags, err := tagCommonRepo.GetTagListByIDs(context.TODO(), []string{testTagList[0].ID})
assert.NoError(t, err)
assert.Equal(t, testTagList[0].SlugName, gotTags[0].SlugName)
}
func Test_tagRepo_GetTagListByName(t *testing.T) {
tagOnce.Do(addTagList)
tagRepo := tag.NewTagRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource))
tagCommonRepo := tag_common.NewTagCommonRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource))
gotTags, err := tagRepo.GetTagListByName(context.TODO(), testTagList[0].SlugName, 1)
gotTags, err := tagCommonRepo.GetTagListByName(context.TODO(), testTagList[0].SlugName, 1, false)
assert.NoError(t, err)
assert.Equal(t, testTagList[0].SlugName, gotTags[0].SlugName)
}
func Test_tagRepo_GetTagListByNames(t *testing.T) {
tagOnce.Do(addTagList)
tagRepo := tag.NewTagRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource))
tagCommonRepo := tag_common.NewTagCommonRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource))
gotTags, err := tagRepo.GetTagListByNames(context.TODO(), []string{testTagList[0].SlugName})
gotTags, err := tagCommonRepo.GetTagListByNames(context.TODO(), []string{testTagList[0].SlugName})
assert.NoError(t, err)
assert.Equal(t, testTagList[0].SlugName, gotTags[0].SlugName)
}
func Test_tagRepo_GetTagPage(t *testing.T) {
tagOnce.Do(addTagList)
tagRepo := tag.NewTagRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource))
tagCommonRepo := tag_common.NewTagCommonRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource))
gotTags, _, err := tagRepo.GetTagPage(context.TODO(), 1, 1, &entity.Tag{SlugName: testTagList[0].SlugName}, "")
gotTags, _, err := tagCommonRepo.GetTagPage(context.TODO(), 1, 1, &entity.Tag{SlugName: testTagList[0].SlugName}, "")
assert.NoError(t, err)
assert.Equal(t, testTagList[0].SlugName, gotTags[0].SlugName)
}
@ -121,7 +122,9 @@ func Test_tagRepo_RemoveTag(t *testing.T) {
err := tagRepo.RemoveTag(context.TODO(), testTagList[1].ID)
assert.NoError(t, err)
_, exist, err := tagRepo.GetTagBySlugName(context.TODO(), testTagList[1].SlugName)
tagCommonRepo := tag_common.NewTagCommonRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource))
_, exist, err := tagCommonRepo.GetTagBySlugName(context.TODO(), testTagList[1].SlugName)
assert.NoError(t, err)
assert.False(t, exist)
}
@ -134,21 +137,22 @@ func Test_tagRepo_UpdateTag(t *testing.T) {
err := tagRepo.UpdateTag(context.TODO(), testTagList[0])
assert.NoError(t, err)
gotTag, exist, err := tagRepo.GetTagByID(context.TODO(), testTagList[0].ID)
tagCommonRepo := tag_common.NewTagCommonRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource))
gotTag, exist, err := tagCommonRepo.GetTagByID(context.TODO(), testTagList[0].ID)
assert.NoError(t, err)
assert.True(t, exist)
assert.Equal(t, testTagList[0].DisplayName, gotTag.DisplayName)
}
func Test_tagRepo_UpdateTagQuestionCount(t *testing.T) {
uniqueIDRepo := unique.NewUniqueIDRepo(testDataSource)
tagRepo := tag.NewTagRepo(testDataSource, uniqueIDRepo)
tagCommonRepo := tag_common.NewTagCommonRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource))
testTagList[0].DisplayName = "golang"
err := tagRepo.UpdateTagQuestionCount(context.TODO(), testTagList[0].ID, 100)
err := tagCommonRepo.UpdateTagQuestionCount(context.TODO(), testTagList[0].ID, 100)
assert.NoError(t, err)
gotTag, exist, err := tagRepo.GetTagByID(context.TODO(), testTagList[0].ID)
gotTag, exist, err := tagCommonRepo.GetTagByID(context.TODO(), testTagList[0].ID)
assert.NoError(t, err)
assert.True(t, exist)
assert.Equal(t, 100, gotTag.QuestionCount)
@ -166,7 +170,9 @@ func Test_tagRepo_UpdateTagSynonym(t *testing.T) {
converter.StringToInt64(testTagList[0].ID), testTagList[0].SlugName)
assert.NoError(t, err)
gotTag, exist, err := tagRepo.GetTagByID(context.TODO(), testTagList[2].ID)
tagCommonRepo := tag_common.NewTagCommonRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource))
gotTag, exist, err := tagCommonRepo.GetTagByID(context.TODO(), testTagList[2].ID)
assert.NoError(t, err)
assert.True(t, exist)
assert.Equal(t, testTagList[0].ID, fmt.Sprintf("%d", gotTag.MainTagID))

View File

@ -3,10 +3,11 @@ package search_common
import (
"context"
"fmt"
"github.com/answerdev/answer/pkg/htmltext"
"strings"
"time"
"github.com/answerdev/answer/pkg/htmltext"
"github.com/answerdev/answer/internal/base/data"
"github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/internal/entity"
@ -67,7 +68,7 @@ func NewSearchRepo(data *data.Data, uniqueIDRepo unique.UniqueIDRepo, userCommon
}
// SearchContents search question and answer data
func (sr *searchRepo) SearchContents(ctx context.Context, words []string, tagID, userID string, votes int, page, size int, order string) (resp []schema.SearchResp, total int64, err error) {
func (sr *searchRepo) SearchContents(ctx context.Context, words []string, tagIDs []string, userID string, votes int, page, size int, order string) (resp []schema.SearchResp, total int64, err error) {
if words = filterWords(words); len(words) == 0 {
return
}
@ -116,10 +117,12 @@ func (sr *searchRepo) SearchContents(ctx context.Context, words []string, tagID,
}
// check tag
if tagID != "" {
if len(tagIDs) > 0 {
b.Join("INNER", "tag_rel", "question.id = tag_rel.object_id").
Where(builder.Eq{"tag_rel.tag_id": tagID})
argsQ = append(argsQ, tagID)
Where(builder.In("tag_rel.tag_id", tagIDs))
for _, tagID := range tagIDs {
argsQ = append(argsQ, tagID)
}
}
// check user
@ -193,7 +196,7 @@ func (sr *searchRepo) SearchContents(ctx context.Context, words []string, tagID,
}
// SearchQuestions search question data
func (sr *searchRepo) SearchQuestions(ctx context.Context, words []string, limitNoAccepted bool, answers, page, size int, order string) (resp []schema.SearchResp, total int64, err error) {
func (sr *searchRepo) SearchQuestions(ctx context.Context, words []string, notAccepted bool, views, answers int, page, size int, order string) (resp []schema.SearchResp, total int64, err error) {
if words = filterWords(words); len(words) == 0 {
return
}
@ -223,11 +226,26 @@ func (sr *searchRepo) SearchQuestions(ctx context.Context, words []string, limit
}
// check need filter has not accepted
if limitNoAccepted {
if notAccepted {
b.And(builder.Eq{"accepted_answer_id": 0})
args = append(args, 0)
}
// check views
if views > -1 {
b.And(builder.Gte{"view_count": views})
args = append(args, views)
}
// check answers
if answers == 0 {
b.And(builder.Eq{"answer_count": answers})
args = append(args, answers)
} else if answers > 0 {
b.And(builder.Gte{"answer_count": answers})
args = append(args, answers)
}
if answers == 0 {
b.And(builder.Eq{"answer_count": 0})
args = append(args, 0)
@ -274,7 +292,7 @@ func (sr *searchRepo) SearchQuestions(ctx context.Context, words []string, limit
}
// SearchAnswers search answer data
func (sr *searchRepo) SearchAnswers(ctx context.Context, words []string, limitAccepted bool, questionID string, page, size int, order string) (resp []schema.SearchResp, total int64, err error) {
func (sr *searchRepo) SearchAnswers(ctx context.Context, words []string, tagIDs []string, accepted bool, questionID string, page, size int, order string) (resp []schema.SearchResp, total int64, err error) {
if words = filterWords(words); len(words) == 0 {
return
}
@ -303,11 +321,23 @@ func (sr *searchRepo) SearchAnswers(ctx context.Context, words []string, limitAc
}
}
if limitAccepted {
// check tags
// check tag
if len(tagIDs) > 0 {
b.Join("INNER", "tag_rel", "question.id = tag_rel.object_id").
Where(builder.In("tag_rel.tag_id", tagIDs))
for _, tagID := range tagIDs {
args = append(args, tagID)
}
}
// check limit accepted
if accepted {
b.Where(builder.Eq{"adopted": schema.AnswerAdoptedEnable})
args = append(args, schema.AnswerAdoptedEnable)
}
// check question id
if questionID != "" {
b.Where(builder.Eq{"question_id": questionID})
args = append(args, questionID)
@ -390,11 +420,12 @@ func (sr *searchRepo) parseResult(ctx context.Context, res []map[string][]byte)
// get tags
err = sr.data.DB.
Select("`display_name`,`slug_name`,`main_tag_slug_name`").
Select("`display_name`,`slug_name`,`main_tag_slug_name`,`recommend`,`reserved`").
Table("tag").
Join("INNER", "tag_rel", "tag.id = tag_rel.tag_id").
Where(builder.Eq{"tag_rel.object_id": r["question_id"]}).
And(builder.Eq{"tag_rel.status": entity.TagRelStatusAvailable}).
UseBool("recommend", "reserved").
Find(&tagsEntity)
if err != nil {
@ -421,6 +452,7 @@ func (sr *searchRepo) parseResult(ctx context.Context, res []map[string][]byte)
object = schema.SearchObject{
ID: string(r["id"]),
QuestionID: string(r["question_id"]),
Title: string(r["title"]),
Excerpt: htmltext.FetchExcerpt(string(r["parsed_text"]), "...", 240),
CreatedAtParsed: tp.Unix(),

View File

@ -4,10 +4,9 @@ import (
"context"
"github.com/answerdev/answer/internal/base/data"
"github.com/answerdev/answer/internal/base/pager"
"github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/internal/entity"
tagcommon "github.com/answerdev/answer/internal/service/tag_common"
"github.com/answerdev/answer/internal/service/tag"
"github.com/answerdev/answer/internal/service/unique"
"github.com/segmentfault/pacman/errors"
"xorm.io/builder"
@ -23,78 +22,13 @@ type tagRepo struct {
func NewTagRepo(
data *data.Data,
uniqueIDRepo unique.UniqueIDRepo,
) tagcommon.TagRepo {
) tag.TagRepo {
return &tagRepo{
data: data,
uniqueIDRepo: uniqueIDRepo,
}
}
// AddTagList add tag
func (tr *tagRepo) AddTagList(ctx context.Context, tagList []*entity.Tag) (err error) {
for _, item := range tagList {
item.ID, err = tr.uniqueIDRepo.GenUniqueIDStr(ctx, item.TableName())
if err != nil {
return err
}
item.RevisionID = "0"
}
_, err = tr.data.DB.Insert(tagList)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// GetTagListByIDs get tag list all
func (tr *tagRepo) GetTagListByIDs(ctx context.Context, ids []string) (tagList []*entity.Tag, err error) {
tagList = make([]*entity.Tag, 0)
session := tr.data.DB.In("id", ids)
session.Where(builder.Eq{"status": entity.TagStatusAvailable})
err = session.Find(&tagList)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// GetTagBySlugName get tag by slug name
func (tr *tagRepo) GetTagBySlugName(ctx context.Context, slugName string) (tagInfo *entity.Tag, exist bool, err error) {
tagInfo = &entity.Tag{}
session := tr.data.DB.Where("slug_name = ?", slugName)
session.Where(builder.Eq{"status": entity.TagStatusAvailable})
exist, err = session.Get(tagInfo)
if err != nil {
return nil, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// GetTagListByName get tag list all like name
func (tr *tagRepo) GetTagListByName(ctx context.Context, name string, limit int) (tagList []*entity.Tag, err error) {
tagList = make([]*entity.Tag, 0)
session := tr.data.DB.Where("slug_name LIKE ?", name+"%")
session.Where(builder.Eq{"status": entity.TagStatusAvailable})
session.Limit(limit).Asc("slug_name")
err = session.Find(&tagList)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// GetTagListByNames get tag list all like name
func (tr *tagRepo) GetTagListByNames(ctx context.Context, names []string) (tagList []*entity.Tag, err error) {
tagList = make([]*entity.Tag, 0)
session := tr.data.DB.In("slug_name", names)
session.Where(builder.Eq{"status": entity.TagStatusAvailable})
err = session.Find(&tagList)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// RemoveTag delete tag
func (tr *tagRepo) RemoveTag(ctx context.Context, tagID string) (err error) {
session := tr.data.DB.Where(builder.Eq{"id": tagID})
@ -114,16 +48,6 @@ func (tr *tagRepo) UpdateTag(ctx context.Context, tag *entity.Tag) (err error) {
return
}
// UpdateTagQuestionCount update tag question count
func (tr *tagRepo) UpdateTagQuestionCount(ctx context.Context, tagID string, questionCount int) (err error) {
cond := &entity.Tag{QuestionCount: questionCount}
_, err = tr.data.DB.Where(builder.Eq{"id": tagID}).MustCols("question_count").Update(cond)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// UpdateTagSynonym update synonym tag
func (tr *tagRepo) UpdateTagSynonym(ctx context.Context, tagSlugNameList []string, mainTagID int64,
mainTagSlugName string,
@ -137,20 +61,6 @@ func (tr *tagRepo) UpdateTagSynonym(ctx context.Context, tagSlugNameList []strin
return
}
// GetTagByID get tag one
func (tr *tagRepo) GetTagByID(ctx context.Context, tagID string) (
tag *entity.Tag, exist bool, err error,
) {
tag = &entity.Tag{}
session := tr.data.DB.Where(builder.Eq{"id": tagID})
session.Where(builder.Eq{"status": entity.TagStatusAvailable})
exist, err = session.Get(tag)
if err != nil {
return nil, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// GetTagList get tag list all
func (tr *tagRepo) GetTagList(ctx context.Context, tag *entity.Tag) (tagList []*entity.Tag, err error) {
tagList = make([]*entity.Tag, 0)
@ -161,33 +71,3 @@ func (tr *tagRepo) GetTagList(ctx context.Context, tag *entity.Tag) (tagList []*
}
return
}
// GetTagPage get tag page
func (tr *tagRepo) GetTagPage(ctx context.Context, page, pageSize int, tag *entity.Tag, queryCond string) (
tagList []*entity.Tag, total int64, err error,
) {
tagList = make([]*entity.Tag, 0)
session := tr.data.DB.NewSession()
if len(tag.SlugName) > 0 {
session.Where(builder.Or(builder.Like{"slug_name", tag.SlugName}, builder.Like{"display_name", tag.SlugName}))
tag.SlugName = ""
}
session.Where(builder.Eq{"status": entity.TagStatusAvailable})
session.Where("main_tag_id = 0") // if this tag is synonym, exclude it
switch queryCond {
case "popular":
session.Desc("question_count")
case "name":
session.Asc("slug_name")
case "newest":
session.Desc("created_at")
}
total, err = pager.Help(page, pageSize, &tagList, tag, session)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}

View File

@ -0,0 +1,210 @@
package tag_common
import (
"context"
"github.com/answerdev/answer/internal/base/data"
"github.com/answerdev/answer/internal/base/pager"
"github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/internal/entity"
tagcommon "github.com/answerdev/answer/internal/service/tag_common"
"github.com/answerdev/answer/internal/service/unique"
"github.com/segmentfault/pacman/errors"
"xorm.io/builder"
)
// tagCommonRepo tag repository
type tagCommonRepo struct {
data *data.Data
uniqueIDRepo unique.UniqueIDRepo
}
// NewTagCommonRepo new repository
func NewTagCommonRepo(
data *data.Data,
uniqueIDRepo unique.UniqueIDRepo,
) tagcommon.TagCommonRepo {
return &tagCommonRepo{
data: data,
uniqueIDRepo: uniqueIDRepo,
}
}
// GetTagListByIDs get tag list all
func (tr *tagCommonRepo) GetTagListByIDs(ctx context.Context, ids []string) (tagList []*entity.Tag, err error) {
tagList = make([]*entity.Tag, 0)
session := tr.data.DB.In("id", ids)
session.Where(builder.Eq{"status": entity.TagStatusAvailable})
err = session.OrderBy("recommend desc,reserved desc,id desc").Find(&tagList)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// GetTagBySlugName get tag by slug name
func (tr *tagCommonRepo) GetTagBySlugName(ctx context.Context, slugName string) (tagInfo *entity.Tag, exist bool, err error) {
tagInfo = &entity.Tag{}
session := tr.data.DB.Where("slug_name = ?", slugName)
session.Where(builder.Eq{"status": entity.TagStatusAvailable})
exist, err = session.Get(tagInfo)
if err != nil {
return nil, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// GetTagListByName get tag list all like name
func (tr *tagCommonRepo) GetTagListByName(ctx context.Context, name string, limit int, hasReserved bool) (tagList []*entity.Tag, err error) {
tagList = make([]*entity.Tag, 0)
cond := &entity.Tag{}
session := tr.data.DB.Where("")
if name != "" {
session.Where("slug_name LIKE ?", name+"%")
} else {
cond.Recommend = true
}
session.Where(builder.Eq{"status": entity.TagStatusAvailable})
session.Limit(limit).Asc("slug_name")
if !hasReserved {
cond.Reserved = false
session.UseBool("recommend", "reserved")
} else {
session.UseBool("recommend")
}
err = session.OrderBy("recommend desc,reserved desc,id desc").Find(&tagList, cond)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
func (tr *tagCommonRepo) GetRecommendTagList(ctx context.Context) (tagList []*entity.Tag, err error) {
tagList = make([]*entity.Tag, 0)
cond := &entity.Tag{}
session := tr.data.DB.Where("")
cond.Recommend = true
// session.Where(builder.Eq{"status": entity.TagStatusAvailable})
session.Asc("slug_name")
session.UseBool("recommend")
err = session.Find(&tagList, cond)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
func (tr *tagCommonRepo) GetReservedTagList(ctx context.Context) (tagList []*entity.Tag, err error) {
tagList = make([]*entity.Tag, 0)
cond := &entity.Tag{}
session := tr.data.DB.Where("")
cond.Reserved = true
// session.Where(builder.Eq{"status": entity.TagStatusAvailable})
session.Asc("slug_name")
session.UseBool("reserved")
err = session.Find(&tagList, cond)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// GetTagListByNames get tag list all like name
func (tr *tagCommonRepo) GetTagListByNames(ctx context.Context, names []string) (tagList []*entity.Tag, err error) {
tagList = make([]*entity.Tag, 0)
session := tr.data.DB.In("slug_name", names).UseBool("recommend", "reserved")
// session.Where(builder.Eq{"status": entity.TagStatusAvailable})
err = session.OrderBy("recommend desc,reserved desc,id desc").Find(&tagList)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// GetTagByID get tag one
func (tr *tagCommonRepo) GetTagByID(ctx context.Context, tagID string) (
tag *entity.Tag, exist bool, err error,
) {
tag = &entity.Tag{}
session := tr.data.DB.Where(builder.Eq{"id": tagID})
session.Where(builder.Eq{"status": entity.TagStatusAvailable})
exist, err = session.Get(tag)
if err != nil {
return nil, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// GetTagPage get tag page
func (tr *tagCommonRepo) GetTagPage(ctx context.Context, page, pageSize int, tag *entity.Tag, queryCond string) (
tagList []*entity.Tag, total int64, err error,
) {
tagList = make([]*entity.Tag, 0)
session := tr.data.DB.NewSession()
if len(tag.SlugName) > 0 {
session.Where(builder.Or(builder.Like{"slug_name", tag.SlugName}, builder.Like{"display_name", tag.SlugName}))
tag.SlugName = ""
}
session.Where(builder.Eq{"status": entity.TagStatusAvailable})
session.Where("main_tag_id = 0") // if this tag is synonym, exclude it
switch queryCond {
case "popular":
session.Desc("question_count")
case "name":
session.Asc("slug_name")
case "newest":
session.Desc("created_at")
}
total, err = pager.Help(page, pageSize, &tagList, tag, session)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// AddTagList add tag
func (tr *tagCommonRepo) AddTagList(ctx context.Context, tagList []*entity.Tag) (err error) {
for _, item := range tagList {
item.ID, err = tr.uniqueIDRepo.GenUniqueIDStr(ctx, item.TableName())
if err != nil {
return err
}
item.RevisionID = "0"
}
_, err = tr.data.DB.Insert(tagList)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// UpdateTagQuestionCount update tag question count
func (tr *tagCommonRepo) UpdateTagQuestionCount(ctx context.Context, tagID string, questionCount int) (err error) {
cond := &entity.Tag{QuestionCount: questionCount}
_, err = tr.data.DB.Where(builder.Eq{"id": tagID}).MustCols("question_count").Update(cond)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
func (tr *tagCommonRepo) UpdateTagsAttribute(ctx context.Context, tags []string, attribute string, value bool) (err error) {
bean := &entity.Tag{}
switch attribute {
case "recommend":
bean.Recommend = value
case "reserved":
bean.Reserved = value
default:
return
}
session := tr.data.DB.In("slug_name", tags).Cols(attribute).UseBool(attribute)
_, err = session.Update(bean)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}

View File

@ -7,6 +7,7 @@ import (
"strings"
"time"
"unicode"
"xorm.io/builder"
"github.com/answerdev/answer/internal/base/data"

View File

@ -28,6 +28,7 @@ type AnswerAPIRouter struct {
siteinfoController *controller.SiteinfoController
notificationController *controller.NotificationController
dashboardController *controller.DashboardController
uploadController *controller.UploadController
}
func NewAnswerAPIRouter(
@ -52,7 +53,7 @@ func NewAnswerAPIRouter(
siteinfoController *controller.SiteinfoController,
notificationController *controller.NotificationController,
dashboardController *controller.DashboardController,
uploadController *controller.UploadController,
) *AnswerAPIRouter {
return &AnswerAPIRouter{
langController: langController,
@ -76,6 +77,7 @@ func NewAnswerAPIRouter(
notificationController: notificationController,
siteinfoController: siteinfoController,
dashboardController: dashboardController,
uploadController: uploadController,
}
}
@ -134,6 +136,7 @@ func (a *AnswerAPIRouter) RegisterUnAuthAnswerAPIRouter(r *gin.RouterGroup) {
//siteinfo
r.GET("/siteinfo", a.siteinfoController.GetSiteInfo)
r.GET("/siteinfo/legal", a.siteinfoController.GetSiteLegalInfo)
}
func (a *AnswerAPIRouter) RegisterAnswerAPIRouter(r *gin.RouterGroup) {
@ -180,8 +183,6 @@ func (a *AnswerAPIRouter) RegisterAnswerAPIRouter(r *gin.RouterGroup) {
r.PUT("/user/password", a.userController.UserModifyPassWord)
r.PUT("/user/info", a.userController.UserUpdateInfo)
r.PUT("/user/interface", a.userController.UserUpdateInterface)
r.POST("/user/avatar/upload", a.userController.UploadUserAvatar)
r.POST("/user/post/file", a.userController.UploadUserPostFile)
r.POST("/user/notice/set", a.userController.UserNoticeSet)
// vote
@ -196,6 +197,9 @@ func (a *AnswerAPIRouter) RegisterAnswerAPIRouter(r *gin.RouterGroup) {
r.GET("/notification/page", a.notificationController.GetList)
r.PUT("/notification/read/state/all", a.notificationController.ClearUnRead)
r.PUT("/notification/read/state", a.notificationController.ClearIDUnRead)
// upload file
r.POST("/file", a.uploadController.UploadFile)
}
func (a *AnswerAPIRouter) RegisterAnswerCmsAPIRouter(r *gin.RouterGroup) {
@ -224,8 +228,14 @@ func (a *AnswerAPIRouter) RegisterAnswerCmsAPIRouter(r *gin.RouterGroup) {
// siteinfo
r.GET("/siteinfo/general", a.siteInfoController.GetGeneral)
r.GET("/siteinfo/interface", a.siteInfoController.GetInterface)
r.GET("/siteinfo/branding", a.siteInfoController.GetSiteBranding)
r.GET("/siteinfo/write", a.siteInfoController.GetSiteWrite)
r.GET("/siteinfo/legal", a.siteInfoController.GetSiteLegal)
r.PUT("/siteinfo/general", a.siteInfoController.UpdateGeneral)
r.PUT("/siteinfo/interface", a.siteInfoController.UpdateInterface)
r.PUT("/siteinfo/branding", a.siteInfoController.UpdateBranding)
r.PUT("/siteinfo/write", a.siteInfoController.UpdateSiteWrite)
r.PUT("/siteinfo/legal", a.siteInfoController.UpdateSiteLegal)
r.GET("/setting/smtp", a.siteInfoController.GetSMTPConfig)
r.PUT("/setting/smtp", a.siteInfoController.UpdateSMTPConfig)

View File

@ -68,24 +68,23 @@ func (a *UIRouter) Register(r *gin.Engine) {
// specify the not router for default routes and redirect
r.NoRoute(func(c *gin.Context) {
name := c.Request.URL.Path
urlPath := c.Request.URL.Path
filePath := ""
var file []byte
var err error
switch name {
switch urlPath {
case "/favicon.ico":
c.Header("content-type", "image/vnd.microsoft.icon")
filePath = UIRootFilePath + name
filePath = UIRootFilePath + urlPath
case "/manifest.json":
filePath = UIRootFilePath + name
filePath = UIRootFilePath + urlPath
case "/install":
// if answer is running by run command user can not access install page.
c.Redirect(http.StatusFound, "/")
return
default:
filePath = UIIndexFilePath
c.Header("content-type", "text/html;charset=utf-8")
}
file, err = ui.Build.ReadFile(filePath)
file, err := ui.Build.ReadFile(filePath)
if err != nil {
log.Error(err)
c.Status(http.StatusNotFound)

View File

@ -10,6 +10,7 @@ type SearchDTO struct {
type SearchObject struct {
ID string `json:"id"`
QuestionID string `json:"question_id"`
Title string `json:"title"`
Excerpt string `json:"excerpt"`
CreatedAtParsed int64 `json:"created_at"`
@ -29,6 +30,8 @@ type TagResp struct {
DisplayName string `json:"display_name"`
// if main tag slug name is not empty, this tag is synonymous with the main tag
MainTagSlugName string `json:"main_tag_slug_name"`
Recommend bool `json:"recommend"`
Reserved bool `json:"reserved"`
}
type SearchResp struct {

View File

@ -24,21 +24,76 @@ func (r *SiteGeneralReq) FormatSiteUrl() {
// SiteInterfaceReq site interface request
type SiteInterfaceReq struct {
Logo string `validate:"omitempty,gt=0,lte=256" form:"logo" json:"logo"`
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"`
}
// SiteBrandingReq site branding request
type SiteBrandingReq struct {
Logo string `validate:"required,gt=0,lte=512" form:"logo" json:"logo"`
MobileLogo string `validate:"omitempty,gt=0,lte=512" form:"mobile_logo" json:"mobile_logo"`
SquareIcon string `validate:"required,gt=0,lte=512" form:"square_icon" json:"square_icon"`
Favicon string `validate:"omitempty,gt=0,lte=512" form:"favicon" json:"favicon"`
}
// SiteWriteReq site write request
type SiteWriteReq struct {
RequiredTag bool `validate:"omitempty" form:"required_tag" json:"required_tag"`
RecommendTags []string `validate:"omitempty" form:"recommend_tags" json:"recommend_tags"`
ReservedTags []string `validate:"omitempty" form:"reserved_tags" json:"reserved_tags"`
UserID string `json:"-"`
}
// SiteLegalReq site branding request
type SiteLegalReq struct {
TermsOfServiceOriginalText string `json:"terms_of_service_original_text"`
TermsOfServiceParsedText string `json:"terms_of_service_parsed_text"`
PrivacyPolicyOriginalText string `json:"privacy_policy_original_text"`
PrivacyPolicyParsedText string `json:"privacy_policy_parsed_text"`
}
// GetSiteLegalInfoReq site site legal request
type GetSiteLegalInfoReq struct {
InfoType string `validate:"required,oneof=tos privacy" form:"info_type"`
}
func (r *GetSiteLegalInfoReq) IsTOS() bool {
return r.InfoType == "tos"
}
func (r *GetSiteLegalInfoReq) IsPrivacy() bool {
return r.InfoType == "privacy"
}
// GetSiteLegalInfoResp get site legal info response
type GetSiteLegalInfoResp struct {
TermsOfServiceOriginalText string `json:"terms_of_service_original_text,omitempty"`
TermsOfServiceParsedText string `json:"terms_of_service_parsed_text,omitempty"`
PrivacyPolicyOriginalText string `json:"privacy_policy_original_text,omitempty"`
PrivacyPolicyParsedText string `json:"privacy_policy_parsed_text,omitempty"`
}
// SiteGeneralResp site general response
type SiteGeneralResp SiteGeneralReq
// SiteInterfaceResp site interface response
type SiteInterfaceResp SiteInterfaceReq
// SiteBrandingResp site branding response
type SiteBrandingResp SiteBrandingReq
// SiteWriteResp site write response
type SiteWriteResp SiteWriteReq
// SiteLegalResp site write response
type SiteLegalResp SiteLegalReq
// SiteInfoResp get site info response
type SiteInfoResp struct {
General *SiteGeneralResp `json:"general"`
Face *SiteInterfaceResp `json:"interface"`
General *SiteGeneralResp `json:"general"`
Interface *SiteInterfaceResp `json:"interface"`
Branding *SiteBrandingResp `json:"branding"`
}
// UpdateSMTPConfigReq get smtp config request

View File

@ -11,7 +11,8 @@ import (
// SearchTagLikeReq get tag list all request
type SearchTagLikeReq struct {
// tag
Tag string `validate:"required,gt=0,lte=35" form:"tag"`
Tag string `validate:"omitempty" form:"tag"`
IsAdmin bool `json:"-"`
}
// GetTagInfoReq get tag info request
@ -24,7 +25,7 @@ type GetTagInfoReq struct {
UserID string `json:"-"`
}
func (r *GetTagInfoReq) Check() (errField *validator.ErrorField, err error) {
func (r *GetTagInfoReq) Check() (errFields []*validator.FormErrorField, err error) {
if len(r.ID) == 0 && len(r.Name) == 0 {
return nil, errors.BadRequest(reason.RequestFormatError)
}
@ -60,6 +61,8 @@ type GetTagResp struct {
MemberActions []*PermissionMemberAction `json:"member_actions"`
// if main tag slug name is not empty, this tag is synonymous with the main tag
MainTagSlugName string `json:"main_tag_slug_name"`
Recommend bool `json:"recommend"`
Reserved bool `json:"reserved"`
}
func (tr *GetTagResp) GetExcerpt() {
@ -95,6 +98,8 @@ type GetTagPageResp struct {
CreatedAt int64 `json:"created_at"`
// updated time
UpdatedAt int64 `json:"updated_at"`
Recommend bool `json:"recommend"`
Reserved bool `json:"reserved"`
}
func (tr *GetTagPageResp) GetExcerpt() {
@ -150,9 +155,9 @@ type UpdateTagReq struct {
UserID string `json:"-"`
}
func (r *UpdateTagReq) Check() (errField *validator.ErrorField, err error) {
func (r *UpdateTagReq) Check() (errFields []*validator.FormErrorField, err error) {
if len(r.EditSummary) == 0 {
r.EditSummary = "tag.edit.summary" // to i18n
r.EditSummary = "tag.edit.summary" // to do i18n
}
return nil, nil
}
@ -217,4 +222,12 @@ type GetFollowingTagsResp struct {
DisplayName string `json:"display_name"`
// if main tag slug name is not empty, this tag is synonymous with the main tag
MainTagSlugName string `json:"main_tag_slug_name"`
Recommend bool `json:"recommend"`
Reserved bool `json:"reserved"`
}
type SearchTagLikeResp struct {
SlugName string `json:"slug_name"`
Recommend bool `json:"recommend"`
Reserved bool `json:"reserved"`
}

View File

@ -219,10 +219,10 @@ var UserStatusShowMsg = map[int]string{
// EmailLogin
type UserEmailLogin struct {
Email string `json:"e_mail" ` // e_mail
Pass string `json:"pass" ` // password
CaptchaID string `json:"captcha_id" ` // captcha_id
CaptchaCode string `json:"captcha_code" ` // captcha_code
Email string `validate:"required,email,gt=0,lte=500" json:"e_mail"` // e_mail
Pass string `validate:"required,gte=8,lte=32" json:"pass"` // password
CaptchaID string `json:"captcha_id"` // captcha_id
CaptchaCode string `json:"captcha_code"` // captcha_code
}
// UserRegisterReq user register request
@ -236,14 +236,16 @@ type UserRegisterReq struct {
IP string `json:"-" `
}
func (u *UserRegisterReq) Check() (errField *validator.ErrorField, err error) {
func (u *UserRegisterReq) Check() (errFields []*validator.FormErrorField, err error) {
// TODO i18n
err = checker.CheckPassword(8, 32, 0, u.Pass)
if err != nil {
return &validator.ErrorField{
Key: "pass",
Value: err.Error(),
}, err
errField := &validator.FormErrorField{
ErrorField: "pass",
ErrorMsg: err.Error(),
}
errFields = append(errFields, errField)
return errFields, err
}
return nil, nil
}
@ -255,14 +257,16 @@ type UserModifyPassWordRequest struct {
Pass string `json:"pass" ` // password
}
func (u *UserModifyPassWordRequest) Check() (errField *validator.ErrorField, err error) {
func (u *UserModifyPassWordRequest) Check() (errFields []*validator.FormErrorField, err error) {
// TODO i18n
err = checker.CheckPassword(8, 32, 0, u.Pass)
if err != nil {
return &validator.ErrorField{
Key: "pass",
Value: err.Error(),
}, err
errField := &validator.FormErrorField{
ErrorField: "pass",
ErrorMsg: err.Error(),
}
errFields = append(errFields, errField)
return errFields, err
}
return nil, nil
}
@ -292,16 +296,17 @@ type AvatarInfo struct {
Custom string `validate:"omitempty,gt=0,lte=200" json:"custom"`
}
func (u *UpdateInfoRequest) Check() (errField *validator.ErrorField, err error) {
func (u *UpdateInfoRequest) Check() (errFields []*validator.FormErrorField, err error) {
if len(u.Username) > 0 {
re := regexp.MustCompile(`^[a-z0-9._-]{4,30}$`)
match := re.MatchString(u.Username)
if !match {
err = errors.BadRequest(reason.UsernameInvalid)
return &validator.ErrorField{
Key: "username",
Value: err.Error(),
}, err
errField := &validator.FormErrorField{
ErrorField: "username",
ErrorMsg: err.Error(),
}
errFields = append(errFields, errField)
return errFields, errors.BadRequest(reason.UsernameInvalid)
}
}
return nil, nil
@ -327,14 +332,16 @@ type UserRePassWordRequest struct {
Content string `json:"-"`
}
func (u *UserRePassWordRequest) Check() (errField *validator.ErrorField, err error) {
func (u *UserRePassWordRequest) Check() (errFields []*validator.FormErrorField, err error) {
// TODO i18n
err = checker.CheckPassword(8, 32, 0, u.Pass)
if err != nil {
return &validator.ErrorField{
Key: "pass",
Value: err.Error(),
}, err
errField := &validator.FormErrorField{
ErrorField: "pass",
ErrorMsg: err.Error(),
}
errFields = append(errFields, errField)
return errFields, err
}
return nil, nil
}
@ -410,8 +417,3 @@ type UserVerifyEmailSendReq struct {
CaptchaID string `validate:"omitempty,gt=0,lte=500" json:"captcha_id"`
CaptchaCode string `validate:"omitempty,gt=0,lte=500" json:"captcha_code"`
}
type UserVerifyEmailErrorResponse struct {
Key string `json:"key"`
Value string `json:"value"`
}

View File

@ -76,9 +76,25 @@ func (as *AnswerService) RemoveAnswer(ctx context.Context, req *schema.RemoveAns
if answerInfo.UserID != req.UserID {
return errors.BadRequest(reason.UnauthorizedError)
}
if answerInfo.VoteCount > 0 {
return errors.BadRequest(reason.UnauthorizedError)
}
if answerInfo.Adopted == schema.AnswerAdoptedEnable {
return errors.BadRequest(reason.UnauthorizedError)
}
questionInfo, exist, err := as.questionRepo.GetQuestion(ctx, answerInfo.QuestionID)
if err != nil {
return errors.BadRequest(reason.UnauthorizedError)
}
if !exist {
return errors.BadRequest(reason.UnauthorizedError)
}
if questionInfo.AnswerCount > 1 {
return errors.BadRequest(reason.UnauthorizedError)
}
if questionInfo.AcceptedAnswerID != "" {
return errors.BadRequest(reason.UnauthorizedError)
}
// user add question count
err = as.questionCommon.UpdateAnswerCount(ctx, answerInfo.QuestionID, -1)

View File

@ -43,6 +43,7 @@ func (as *AuthService) GetUserCacheInfo(ctx context.Context, accessToken string)
log.Infof("user status updated: %+v", cacheInfo)
userCacheInfo.UserStatus = cacheInfo.UserStatus
userCacheInfo.EmailStatus = cacheInfo.EmailStatus
userCacheInfo.IsAdmin = cacheInfo.IsAdmin
// update current user cache info
err := as.authRepo.SetUserCacheInfo(ctx, accessToken, userCacheInfo)
if err != nil {
@ -58,6 +59,10 @@ func (as *AuthService) SetUserCacheInfo(ctx context.Context, userInfo *entity.Us
return accessToken, err
}
func (as *AuthService) SetUserStatus(ctx context.Context, userInfo *entity.UserCacheInfo) (err error) {
return as.authRepo.SetUserStatus(ctx, userInfo.UserID, userInfo)
}
func (as *AuthService) UpdateUserCacheInfo(ctx context.Context, token string, userInfo *entity.UserCacheInfo) (err error) {
err = as.authRepo.SetUserCacheInfo(ctx, token, userInfo)
if err != nil {

View File

@ -76,17 +76,16 @@ func (ds *DashboardService) StatisticalByCache(ctx context.Context) (*schema.Das
if err != nil {
info, statisticalErr := ds.Statistical(ctx)
if statisticalErr != nil {
return dashboardInfo, err
return nil, statisticalErr
}
setCacheErr := ds.SetCache(ctx, info)
if setCacheErr != nil {
log.Error("ds.SetCache", setCacheErr)
if setCacheErr := ds.SetCache(ctx, info); setCacheErr != nil {
log.Errorf("set dashboard statistical failed: %s", setCacheErr)
}
return info, err
return info, nil
}
err = json.Unmarshal([]byte(infoStr), dashboardInfo)
if err != nil {
return dashboardInfo, err
if err = json.Unmarshal([]byte(infoStr), dashboardInfo); err != nil {
log.Errorf("parsing dashboard information failed: %s", err)
return nil, errors.InternalServer(reason.UnknownError)
}
startTime := time.Now().Unix() - schema.AppStartTime.Unix()
dashboardInfo.AppStartTime = fmt.Sprintf("%d", startTime)

View File

@ -15,7 +15,7 @@ type FollowRepo interface {
}
type FollowService struct {
tagRepo tagcommon.TagRepo
tagRepo tagcommon.TagCommonRepo
followRepo FollowRepo
followCommonRepo activity_common.FollowRepo
}
@ -23,7 +23,7 @@ type FollowService struct {
func NewFollowService(
followRepo FollowRepo,
followCommonRepo activity_common.FollowRepo,
tagRepo tagcommon.TagRepo,
tagRepo tagcommon.TagCommonRepo,
) *FollowService {
return &FollowService{
followRepo: followRepo,

View File

@ -19,7 +19,7 @@ type ObjService struct {
answerRepo answercommon.AnswerRepo
questionRepo questioncommon.QuestionRepo
commentRepo comment_common.CommentCommonRepo
tagRepo tagcommon.TagRepo
tagRepo tagcommon.TagCommonRepo
}
// NewObjService new object service
@ -27,7 +27,7 @@ func NewObjService(
answerRepo answercommon.AnswerRepo,
questionRepo questioncommon.QuestionRepo,
commentRepo comment_common.CommentCommonRepo,
tagRepo tagcommon.TagRepo) *ObjService {
tagRepo tagcommon.TagCommonRepo) *ObjService {
return &ObjService{
answerRepo: answerRepo,
questionRepo: questionRepo,

View File

@ -24,6 +24,7 @@ import (
"github.com/answerdev/answer/internal/service/revision_common"
"github.com/answerdev/answer/internal/service/siteinfo"
"github.com/answerdev/answer/internal/service/siteinfo_common"
"github.com/answerdev/answer/internal/service/search_parser"
"github.com/answerdev/answer/internal/service/tag"
tagcommon "github.com/answerdev/answer/internal/service/tag_common"
"github.com/answerdev/answer/internal/service/uploader"
@ -57,6 +58,7 @@ var ProviderSetService = wire.NewSet(
revision_common.NewRevisionService,
NewRevisionService,
rank.NewRankService,
search_parser.NewSearchParser,
NewSearchService,
meta.NewMetaService,
object_info.NewObjService,

View File

@ -3,6 +3,7 @@ package service
import (
"encoding/json"
"fmt"
"strings"
"time"
"github.com/answerdev/answer/internal/base/constant"
@ -105,6 +106,16 @@ func (qs *QuestionService) CloseMsgList(ctx context.Context, lang i18n.Language)
// AddQuestion add question
func (qs *QuestionService) AddQuestion(ctx context.Context, req *schema.QuestionAdd) (questionInfo *schema.QuestionInfo, err error) {
recommendExist, err := qs.tagCommon.ExistRecommend(ctx, req.Tags)
if err != nil {
return
}
if !recommendExist {
err = fmt.Errorf("recommend is not exist")
err = errors.BadRequest(reason.RecommendTagNotExist).WithError(err).WithStack()
return
}
questionInfo = &schema.QuestionInfo{}
question := &entity.Question{}
now := time.Now()
@ -215,6 +226,28 @@ func (qs *QuestionService) UpdateQuestion(ctx context.Context, req *schema.Quest
if dbinfo.UserID != req.UserID {
return
}
//CheckChangeTag
oldTags, err := qs.tagCommon.GetObjectEntityTag(ctx, question.ID)
if err != nil {
return
}
tagNameList := make([]string, 0)
for _, tag := range req.Tags {
tagNameList = append(tagNameList, tag.SlugName)
}
Tags, err := qs.tagCommon.GetTagListByNames(ctx, tagNameList)
if err != nil {
return
}
CheckTag, CheckTaglist := qs.CheckChangeTag(ctx, oldTags, Tags)
if !CheckTag {
err = errors.BadRequest(reason.UnauthorizedError).WithMsg(fmt.Sprintf("tag [%s] cannot be modified",
strings.Join(CheckTaglist, ",")))
return
}
//update question to db
err = qs.questionRepo.UpdateQuestion(ctx, question, []string{"title", "original_text", "parsed_text", "updated_at"})
if err != nil {
return
@ -266,6 +299,10 @@ func (qs *QuestionService) ChangeTag(ctx context.Context, objectTagData *schema.
return qs.tagCommon.ObjectChangeTag(ctx, objectTagData)
}
func (qs *QuestionService) CheckChangeTag(ctx context.Context, oldobjectTagData, objectTagData []*entity.Tag) (bool, []string) {
return qs.tagCommon.ObjectCheckChangeTag(ctx, oldobjectTagData, objectTagData)
}
func (qs *QuestionService) SearchUserList(ctx context.Context, userName, order string, page, pageSize int, loginUserID string) ([]*schema.UserQuestionInfo, int64, error) {
userlist := make([]*schema.UserQuestionInfo, 0)
@ -502,14 +539,13 @@ func (qs *QuestionService) SimilarQuestion(ctx context.Context, questionID strin
// SearchList
func (qs *QuestionService) SearchList(ctx context.Context, req *schema.QuestionSearch, loginUserID string) ([]*schema.QuestionInfo, int64, error) {
if len(req.Tag) > 0 {
taginfo, has, err := qs.tagCommon.GetTagListByName(ctx, req.Tag)
tagInfo, has, err := qs.tagCommon.GetTagBySlugName(ctx, strings.ToLower(req.Tag))
if err != nil {
log.Error("tagCommon.GetTagListByNames error", err)
}
if has {
req.TagIDs = append(req.TagIDs, taginfo.ID)
req.TagIDs = append(req.TagIDs, tagInfo.ID)
}
}
list := make([]*schema.QuestionInfo, 0)
if req.UserName != "" {

View File

@ -113,6 +113,8 @@ func (rs *RevisionService) parseItem(ctx context.Context, item *schema.GetRevisi
ParsedText: tag.ParsedText,
FollowCount: tag.FollowCount,
QuestionCount: tag.QuestionCount,
Recommend: tag.Recommend,
Reserved: tag.Reserved,
}
tagInfo.GetExcerpt()
item.ContentParsed = tagInfo

View File

@ -1,55 +0,0 @@
package search
import (
"context"
"strings"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/search_common"
)
type AcceptedAnswerSearch struct {
repo search_common.SearchRepo
w string
page int
size int
order string
}
func NewAcceptedAnswerSearch(repo search_common.SearchRepo) *AcceptedAnswerSearch {
return &AcceptedAnswerSearch{
repo: repo,
}
}
func (s *AcceptedAnswerSearch) Parse(dto *schema.SearchDTO) (ok bool) {
var (
q,
w,
p string
)
q = dto.Query
w = dto.Query
p = `isaccepted:yes`
if strings.Index(q, p) == 0 {
ok = true
w = strings.TrimPrefix(q, p)
}
s.w = strings.TrimSpace(w)
s.page = dto.Page
s.size = dto.Size
s.order = dto.Order
return
}
func (s *AcceptedAnswerSearch) Search(ctx context.Context) (resp []schema.SearchResp, total int64, err error) {
words := strings.Split(s.w, " ")
if len(words) > 3 {
words = words[:4]
}
return s.repo.SearchAnswers(ctx, words, true, "", s.page, s.size, s.order)
}

View File

@ -1,54 +0,0 @@
package search
import (
"context"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/search_common"
"strings"
)
type AnswerSearch struct {
repo search_common.SearchRepo
w string
page int
size int
order string
}
func NewAnswerSearch(repo search_common.SearchRepo) *AnswerSearch {
return &AnswerSearch{
repo: repo,
}
}
func (s *AnswerSearch) Parse(dto *schema.SearchDTO) (ok bool) {
var (
q,
w,
p string
)
q = dto.Query
w = dto.Query
p = `is:answer`
if strings.Index(q, p) == 0 {
ok = true
w = strings.TrimPrefix(q, p)
}
s.w = strings.TrimSpace(w)
s.page = dto.Page
s.size = dto.Size
s.order = dto.Order
return
}
func (s *AnswerSearch) Search(ctx context.Context) (resp []schema.SearchResp, total int64, err error) {
words := strings.Split(s.w, " ")
if len(words) > 3 {
words = words[:4]
}
return s.repo.SearchAnswers(ctx, words, false, "", s.page, s.size, s.order)
}

View File

@ -1,65 +0,0 @@
package search
import (
"context"
"regexp"
"strings"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/search_common"
"github.com/answerdev/answer/pkg/converter"
)
type AnswersSearch struct {
repo search_common.SearchRepo
exp int
w string
page int
size int
order string
}
func NewAnswersSearch(repo search_common.SearchRepo) *AnswersSearch {
return &AnswersSearch{
repo: repo,
}
}
func (s *AnswersSearch) Parse(dto *schema.SearchDTO) (ok bool) {
var (
q,
w,
p,
exp string
)
q = dto.Query
w = dto.Query
p = `(?m)^answers:([0-9]+)`
re := regexp.MustCompile(p)
res := re.FindStringSubmatch(q)
if len(res) == 2 {
exp = res[1]
trimLen := len(res[0])
w = q[trimLen:]
ok = true
}
s.exp = converter.StringToInt(exp)
s.w = strings.TrimSpace(w)
s.page = dto.Page
s.size = dto.Size
s.order = dto.Order
return
}
func (s *AnswersSearch) Search(ctx context.Context) (resp []schema.SearchResp, total int64, err error) {
words := strings.Split(s.w, " ")
if len(words) > 3 {
words = words[:4]
}
return s.repo.SearchQuestions(ctx, words, false, s.exp, s.page, s.size, s.order)
}

View File

@ -1,90 +0,0 @@
package search
import (
"context"
"regexp"
"strings"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/search_common"
usercommon "github.com/answerdev/answer/internal/service/user_common"
)
type AuthorSearch struct {
repo search_common.SearchRepo
userCommon *usercommon.UserCommon
exp string
w string
page int
size int
order string
}
func NewAuthorSearch(repo search_common.SearchRepo, userCommon *usercommon.UserCommon) *AuthorSearch {
return &AuthorSearch{
repo: repo,
userCommon: userCommon,
}
}
// Parse
// example: "user:12345" -> {exp="" w="12345"}
func (s *AuthorSearch) Parse(dto *schema.SearchDTO) (ok bool) {
var (
exp,
q,
w,
p,
me,
name string
)
exp = ""
q = dto.Query
w = q
p = `(?m)^user:([a-z0-9._-]+)`
me = "user:me"
re := regexp.MustCompile(p)
res := re.FindStringSubmatch(q)
if len(res) == 2 {
name = res[1]
user, has, err := s.userCommon.GetUserBasicInfoByUserName(nil, name)
if err == nil && has {
exp = user.ID
trimLen := len(res[0])
w = q[trimLen:]
ok = true
}
} else if strings.Index(q, me) == 0 {
exp = dto.UserID
w = strings.TrimPrefix(q, me)
ok = true
}
w = strings.TrimSpace(w)
s.exp = exp
s.w = w
s.page = dto.Page
s.size = dto.Size
s.order = dto.Order
return
}
func (s *AuthorSearch) Search(ctx context.Context) (resp []schema.SearchResp, total int64, err error) {
var (
words []string
)
if len(s.exp) == 0 {
return
}
words = strings.Split(s.w, " ")
if len(words) > 3 {
words = words[:4]
}
resp, total, err = s.repo.SearchContents(ctx, words, "", s.exp, -1, s.page, s.size, s.order)
return
}

View File

@ -1,65 +0,0 @@
package search
import (
"context"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/search_common"
"regexp"
"strings"
)
type InQuestionSearch struct {
repo search_common.SearchRepo
w string
exp string
page int
size int
order string
}
func NewInQuestionSearch(repo search_common.SearchRepo) *InQuestionSearch {
return &InQuestionSearch{
repo: repo,
}
}
func (s *InQuestionSearch) Parse(dto *schema.SearchDTO) (ok bool) {
var (
w,
q,
p,
exp string
)
q = dto.Query
w = dto.Query
p = `(?m)^inquestion:([0-9]+)`
re := regexp.MustCompile(p)
res := re.FindStringSubmatch(q)
if len(res) == 2 {
exp = res[1]
trimLen := len(res[0])
w = q[trimLen:]
ok = true
}
s.exp = exp
s.w = strings.TrimSpace(w)
s.page = dto.Page
s.size = dto.Size
s.order = dto.Order
return
}
func (s *InQuestionSearch) Search(ctx context.Context) (resp []schema.SearchResp, total int64, err error) {
var (
words []string
)
words = strings.Split(s.w, " ")
if len(words) > 3 {
words = words[:4]
}
return s.repo.SearchAnswers(ctx, words, false, s.exp, s.page, s.size, s.order)
}

View File

@ -1,58 +0,0 @@
package search
import (
"context"
"strings"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/search_common"
)
type NotAcceptedQuestion struct {
repo search_common.SearchRepo
w string
page int
size int
order string
}
func NewNotAcceptedQuestion(repo search_common.SearchRepo) *NotAcceptedQuestion {
return &NotAcceptedQuestion{
repo: repo,
}
}
func (s *NotAcceptedQuestion) Parse(dto *schema.SearchDTO) (ok bool) {
var (
q,
w,
p string
)
q = dto.Query
w = dto.Query
p = `hasaccepted:no`
if strings.Index(q, p) == 0 {
ok = true
w = strings.TrimPrefix(q, p)
}
s.w = strings.TrimSpace(w)
s.page = dto.Page
s.size = dto.Size
s.order = dto.Order
return
}
func (s *NotAcceptedQuestion) Search(ctx context.Context) (resp []schema.SearchResp, total int64, err error) {
var (
words []string
)
words = strings.Split(s.w, " ")
if len(words) > 3 {
words = words[:4]
}
return s.repo.SearchQuestions(ctx, words, true, -1, s.page, s.size, s.order)
}

View File

@ -1,47 +0,0 @@
package search
import (
"context"
"strings"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/search_common"
)
type ObjectSearch struct {
repo search_common.SearchRepo
w string
page int
size int
order string
}
func NewObjectSearch(repo search_common.SearchRepo) *ObjectSearch {
return &ObjectSearch{
repo: repo,
}
}
func (s *ObjectSearch) Parse(dto *schema.SearchDTO) (ok bool) {
var (
w string
)
w = strings.TrimSpace(dto.Query)
if len(w) > 0 {
ok = true
}
s.w = w
s.page = dto.Page
s.size = dto.Size
s.order = dto.Order
return
}
func (s *ObjectSearch) Search(ctx context.Context) (resp []schema.SearchResp, total int64, err error) {
words := strings.Split(s.w, " ")
if len(words) > 3 {
words = words[:4]
}
return s.repo.SearchContents(ctx, words, "", "", -1, s.page, s.size, s.order)
}

View File

@ -1,55 +0,0 @@
package search
import (
"context"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/search_common"
"strings"
)
type QuestionSearch struct {
repo search_common.SearchRepo
w string
page int
size int
order string
}
func NewQuestionSearch(repo search_common.SearchRepo) *QuestionSearch {
return &QuestionSearch{
repo: repo,
}
}
func (s *QuestionSearch) Parse(dto *schema.SearchDTO) (ok bool) {
var (
q,
w,
p string
)
q = dto.Query
w = dto.Query
p = `is:question`
if strings.Index(q, p) == 0 {
ok = true
w = strings.TrimPrefix(q, p)
}
s.w = strings.TrimSpace(w)
s.page = dto.Page
s.size = dto.Size
s.order = dto.Order
return
}
func (s *QuestionSearch) Search(ctx context.Context) (resp []schema.SearchResp, total int64, err error) {
words := strings.Split(s.w, " ")
if len(words) > 3 {
words = words[:4]
}
return s.repo.SearchQuestions(ctx, words, false, -1, s.page, s.size, s.order)
}

View File

@ -1,63 +0,0 @@
package search
import (
"context"
"regexp"
"strings"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/search_common"
"github.com/answerdev/answer/pkg/converter"
)
type ScoreSearch struct {
repo search_common.SearchRepo
exp int
w string
page int
size int
order string
}
func NewScoreSearch(repo search_common.SearchRepo) *ScoreSearch {
return &ScoreSearch{
repo: repo,
}
}
func (s *ScoreSearch) Parse(dto *schema.SearchDTO) (ok bool) {
exp := ""
q := dto.Query
w := q
p := `(?m)^score:([0-9]+)`
re := regexp.MustCompile(p)
res := re.FindStringSubmatch(w)
if len(res) == 2 {
exp = res[1]
trimLen := len(res[0])
w = q[trimLen:]
ok = true
}
w = strings.TrimSpace(w)
s.exp = converter.StringToInt(exp)
s.w = w
s.page = dto.Page
s.size = dto.Size
s.order = dto.Order
return
}
func (s *ScoreSearch) Search(ctx context.Context) (resp []schema.SearchResp, total int64, err error) {
var (
words []string
)
words = strings.Split(s.w, " ")
if len(words) > 3 {
words = words[:4]
}
resp, total, err = s.repo.SearchContents(ctx, words, "", "", s.exp, s.page, s.size, s.order)
return
}

View File

@ -9,27 +9,28 @@ import (
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/activity_common"
"github.com/answerdev/answer/internal/service/search_common"
tagcommon "github.com/answerdev/answer/internal/service/tag_common"
"github.com/answerdev/answer/internal/service/tag_common"
)
type TagSearch struct {
repo search_common.SearchRepo
tagRepo tagcommon.TagRepo
followCommon activity_common.FollowRepo
page int
size int
exp string
w string
userID string
Extra schema.GetTagPageResp
order string
repo search_common.SearchRepo
tagCommonService *tag_common.TagCommonService
followCommon activity_common.FollowRepo
page int
size int
exp string
w string
userID string
Extra schema.GetTagPageResp
order string
}
func NewTagSearch(repo search_common.SearchRepo, tagRepo tagcommon.TagRepo, followCommon activity_common.FollowRepo) *TagSearch {
func NewTagSearch(repo search_common.SearchRepo,
tagCommonService *tag_common.TagCommonService, followCommon activity_common.FollowRepo) *TagSearch {
return &TagSearch{
repo: repo,
tagRepo: tagRepo,
followCommon: followCommon,
repo: repo,
tagCommonService: tagCommonService,
followCommon: followCommon,
}
}
@ -65,7 +66,7 @@ func (ts *TagSearch) Search(ctx context.Context) (resp []schema.SearchResp, tota
tag *entity.Tag
exists, followed bool
)
tag, exists, err = ts.tagRepo.GetTagBySlugName(ctx, ts.exp)
tag, exists, err = ts.tagCommonService.GetTagBySlugName(ctx, ts.exp)
if err != nil {
return
}
@ -82,6 +83,8 @@ func (ts *TagSearch) Search(ctx context.Context) (resp []schema.SearchResp, tota
ParsedText: tag.ParsedText,
QuestionCount: tag.QuestionCount,
IsFollower: followed,
Recommend: tag.Recommend,
Reserved: tag.Reserved,
}
ts.Extra.GetExcerpt()

View File

@ -1,47 +0,0 @@
package search
import (
"context"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/search_common"
"regexp"
"strings"
)
type ViewsSearch struct {
repo search_common.SearchRepo
exp string
q string
order string
}
func NewViewsSearch(repo search_common.SearchRepo) *ViewsSearch {
return &ViewsSearch{
repo: repo,
}
}
func (s *ViewsSearch) Parse(dto *schema.SearchDTO) (ok bool) {
exp := ""
w := dto.Query
q := w
p := `(?m)^views:([0-9]+)`
re := regexp.MustCompile(p)
res := re.FindStringSubmatch(q)
if len(res) == 2 {
exp = res[1]
trimLen := len(res[0])
q = w[trimLen:]
ok = true
}
q = strings.TrimSpace(q)
s.exp = exp
s.q = q
s.order = dto.Order
return
}
func (s *ViewsSearch) Search(ctx context.Context) (resp []schema.SearchResp, total int64, err error) {
return
}

View File

@ -1,59 +0,0 @@
package search
import (
"context"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/search_common"
)
type WithinSearch struct {
repo search_common.SearchRepo
w string
page int
size int
order string
}
func NewWithinSearch(repo search_common.SearchRepo) *WithinSearch {
return &WithinSearch{
repo: repo,
}
}
func (s *WithinSearch) Parse(dto *schema.SearchDTO) (ok bool) {
var (
q string
w []rune
hasEnd bool
)
q = dto.Query
if q[0:1] == `"` {
for _, v := range []rune(q) {
if len(w) == 0 && string(v) == `"` {
continue
} else if string(v) == `"` {
hasEnd = true
break
} else {
w = append(w, v)
}
}
}
if hasEnd {
ok = true
}
s.w = string(w)
s.page = dto.Page
s.size = dto.Size
s.order = dto.Order
return
}
func (s *WithinSearch) Search(ctx context.Context) (resp []schema.SearchResp, total int64, err error) {
return s.repo.SearchContents(ctx, []string{s.w}, "", "", -1, s.page, s.size, s.order)
}

View File

@ -6,7 +6,7 @@ import (
)
type SearchRepo interface {
SearchContents(ctx context.Context, words []string, tagID, userID string, votes, page, size int, order string) (resp []schema.SearchResp, total int64, err error)
SearchQuestions(ctx context.Context, words []string, limitNoAccepted bool, answers, page, size int, order string) (resp []schema.SearchResp, total int64, err error)
SearchAnswers(ctx context.Context, words []string, limitAccepted bool, questionID string, page, size int, order string) (resp []schema.SearchResp, total int64, err error)
SearchContents(ctx context.Context, words []string, tagIDs []string, userID string, votes, page, size int, order string) (resp []schema.SearchResp, total int64, err error)
SearchQuestions(ctx context.Context, words []string, notAccepted bool, views, answers int, page, size int, order string) (resp []schema.SearchResp, total int64, err error)
SearchAnswers(ctx context.Context, words []string, tagIDs []string, accepted bool, questionID string, page, size int, order string) (resp []schema.SearchResp, total int64, err error)
}

View File

@ -0,0 +1,360 @@
package search_parser
import (
"context"
"regexp"
"strings"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/tag_common"
usercommon "github.com/answerdev/answer/internal/service/user_common"
"github.com/answerdev/answer/pkg/converter"
)
type SearchParser struct {
tagCommonService *tag_common.TagCommonService
userCommon *usercommon.UserCommon
}
func NewSearchParser(tagCommonService *tag_common.TagCommonService, userCommon *usercommon.UserCommon) *SearchParser {
return &SearchParser{
tagCommonService: tagCommonService,
userCommon: userCommon,
}
}
// ParseStructure parse search structure, maybe match one of type all/questions/answers,
// but if match two type, it will return false
func (sp *SearchParser) ParseStructure(dto *schema.SearchDTO) (
searchType string,
// search all
userID string,
votes int,
// search questions
notAccepted bool,
isQuestion bool,
views,
answers int,
// search answers
accepted bool,
questionID string,
isAnswer bool,
// common fields
tags,
words []string,
) {
var (
query = dto.Query
currentUserID = dto.UserID
all = 0
q = 0
a = 0
withWords = []string{}
limitWords = 5
)
// match tags
tags = sp.parseTags(&query)
// match all
userID = sp.parseUserID(&query, currentUserID)
if userID != "" {
searchType = "all"
all = 1
}
votes = sp.parseVotes(&query)
if votes != -1 {
searchType = "all"
all = 1
}
withWords = sp.parseWithin(&query)
if len(withWords) > 0 {
searchType = "all"
all = 1
}
// match questions
notAccepted = sp.parseNotAccepted(&query)
if notAccepted {
searchType = "question"
q = 1
}
isQuestion = sp.parseIsQuestion(&query)
if isQuestion {
searchType = "question"
q = 1
}
views = sp.parseViews(&query)
if views != -1 {
searchType = "question"
q = 1
}
answers = sp.parseAnswers(&query)
if answers != -1 {
searchType = "question"
q = 1
}
// match answers
accepted = sp.parseAccepted(&query)
if accepted {
searchType = "answer"
a = 1
}
questionID = sp.parseQuestionID(&query)
if questionID != "" {
searchType = "answer"
a = 1
}
isAnswer = sp.parseIsAnswer(&query)
if isAnswer {
searchType = "answer"
a = 1
}
words = strings.Split(query, " ")
if len(withWords) > 0 {
words = append(withWords, words...)
}
// check limit words
if len(words) > limitWords {
words = words[:limitWords]
}
// check tags' search is all or question
if len(tags) > 0 {
if len(words) > 0 {
searchType = "all"
all = 1
} else {
searchType = "question"
q = 1
}
}
// check match types greater than 1
if all+q+a > 1 {
searchType = ""
}
// check not match
if all+q+a == 0 && len(words) > 0 {
searchType = "all"
}
return
}
// parseTags parse search tags, return tag ids array
func (sp *SearchParser) parseTags(query *string) (tags []string) {
var (
// expire tag pattern
exprTag = `(?m)\[([a-zA-Z0-9-\+\.#]+)\]{1}?`
q = *query
limit = 5
)
re := regexp.MustCompile(exprTag)
res := re.FindAllStringSubmatch(q, -1)
if len(res) == 0 {
return
}
tags = make([]string, len(res))
for i, item := range res {
tag, exists, err := sp.tagCommonService.GetTagBySlugName(context.TODO(), item[1])
if err != nil || !exists {
continue
}
tags[i] = tag.ID
}
// limit maximum 5 tags
if len(tags) > limit {
tags = tags[:limit]
}
q = strings.TrimSpace(re.ReplaceAllString(q, ""))
*query = q
return
}
// parseUserID return user id or current login user id
func (sp *SearchParser) parseUserID(query *string, currentUserID string) (userID string) {
var (
exprUserID = `(?m)^user:([a-z0-9._-]+)`
exprMe = "user:me"
q = *query
)
re := regexp.MustCompile(exprUserID)
res := re.FindStringSubmatch(q)
if len(res) == 2 {
name := res[1]
user, has, err := sp.userCommon.GetUserBasicInfoByUserName(nil, name)
if err == nil && has {
userID = user.ID
q = re.ReplaceAllString(q, "")
}
} else if strings.Index(q, exprMe) != -1 {
userID = currentUserID
q = strings.ReplaceAll(q, exprMe, "")
}
*query = strings.TrimSpace(q)
return
}
// parseVotes return the votes of search query
func (sp *SearchParser) parseVotes(query *string) (votes int) {
var (
expr = `(?m)^score:([0-9]+)`
q = *query
)
votes = -1
re := regexp.MustCompile(expr)
res := re.FindStringSubmatch(q)
if len(res) == 2 {
votes = converter.StringToInt(res[1])
q = re.ReplaceAllString(q, "")
}
*query = strings.TrimSpace(q)
return
}
// parseWithin parse quotes within words like: "hello world"
func (sp *SearchParser) parseWithin(query *string) (words []string) {
var (
q = *query
expr = `(?U)(".+")`
)
re := regexp.MustCompile(expr)
matches := re.FindAllStringSubmatch(q, -1)
words = []string{}
for _, match := range matches {
words = append(words, match[1])
}
q = re.ReplaceAllString(q, "")
*query = strings.TrimSpace(q)
return
}
// parseNotAccepted return the question has not accepted the answer
func (sp *SearchParser) parseNotAccepted(query *string) (notAccepted bool) {
var (
q = *query
expr = `hasaccepted:no`
)
if strings.Index(q, expr) != -1 {
q = strings.ReplaceAll(q, expr, "")
notAccepted = true
}
*query = strings.TrimSpace(q)
return
}
// parseIsQuestion check the result if only limit question or not
func (sp *SearchParser) parseIsQuestion(query *string) (isQuestion bool) {
var (
q = *query
expr = `is:question`
)
if strings.Index(q, expr) == 0 {
q = strings.ReplaceAll(q, expr, "")
isQuestion = true
}
*query = strings.TrimSpace(q)
return
}
// parseViews check search has views or not
func (sp *SearchParser) parseViews(query *string) (views int) {
var (
q = *query
expr = `(?m)^views:([0-9]+)`
)
views = -1
re := regexp.MustCompile(expr)
res := re.FindStringSubmatch(q)
if len(res) == 2 {
views = converter.StringToInt(res[1])
q = re.ReplaceAllString(q, "")
}
*query = strings.TrimSpace(q)
return
}
// parseAnswers check whether specified answer count for question
func (sp *SearchParser) parseAnswers(query *string) (answers int) {
var (
q = *query
expr = `(?m)^answers:([0-9]+)`
)
answers = -1
re := regexp.MustCompile(expr)
res := re.FindStringSubmatch(q)
if len(res) == 2 {
answers = converter.StringToInt(res[1])
q = re.ReplaceAllString(q, "")
}
*query = strings.TrimSpace(q)
return
}
// parseAccepted check the search is limit accepted answer or not
func (sp *SearchParser) parseAccepted(query *string) (accepted bool) {
var (
q = *query
expr = `isaccepted:yes`
)
if strings.Index(q, expr) != -1 {
accepted = true
strings.ReplaceAll(q, expr, "")
}
*query = strings.TrimSpace(q)
return
}
// parseQuestionID check whether specified question's id
func (sp *SearchParser) parseQuestionID(query *string) (questionID string) {
var (
q = *query
expr = `(?m)^inquestion:([0-9]+)`
)
re := regexp.MustCompile(expr)
res := re.FindStringSubmatch(q)
if len(res) == 2 {
questionID = res[1]
q = re.ReplaceAllString(q, "")
}
*query = strings.TrimSpace(q)
return
}
// parseIsAnswer check the result if only limit answer or not
func (sp *SearchParser) parseIsAnswer(query *string) (isAnswer bool) {
var (
q = *query
expr = `is:answer`
)
if strings.Index(q, expr) != -1 {
isAnswer = true
q = strings.ReplaceAll(q, expr, "")
}
*query = strings.TrimSpace(q)
return
}

View File

@ -2,92 +2,61 @@ package service
import (
"context"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/activity_common"
"github.com/answerdev/answer/internal/service/search"
"github.com/answerdev/answer/internal/service/search_common"
tagcommon "github.com/answerdev/answer/internal/service/tag_common"
usercommon "github.com/answerdev/answer/internal/service/user_common"
"github.com/answerdev/answer/internal/service/search_parser"
)
type Search interface {
Parse(dto *schema.SearchDTO) (ok bool)
Search(ctx context.Context) (resp []schema.SearchResp, total int64, err error)
}
type SearchService struct {
searchRepo search_common.SearchRepo
tagSearch *search.TagSearch
withinSearch *search.WithinSearch
authorSearch *search.AuthorSearch
scoreSearch *search.ScoreSearch
answersSearch *search.AnswersSearch
notAcceptedQuestion *search.NotAcceptedQuestion
acceptedAnswerSearch *search.AcceptedAnswerSearch
inQuestionSearch *search.InQuestionSearch
questionSearch *search.QuestionSearch
answerSearch *search.AnswerSearch
viewsSearch *search.ViewsSearch
objectSearch *search.ObjectSearch
searchParser *search_parser.SearchParser
searchRepo search_common.SearchRepo
}
func NewSearchService(
searchParser *search_parser.SearchParser,
searchRepo search_common.SearchRepo,
tagRepo tagcommon.TagRepo,
userCommon *usercommon.UserCommon,
followCommon activity_common.FollowRepo,
) *SearchService {
return &SearchService{
searchRepo: searchRepo,
tagSearch: search.NewTagSearch(searchRepo, tagRepo, followCommon),
withinSearch: search.NewWithinSearch(searchRepo),
authorSearch: search.NewAuthorSearch(searchRepo, userCommon),
scoreSearch: search.NewScoreSearch(searchRepo),
answersSearch: search.NewAnswersSearch(searchRepo),
acceptedAnswerSearch: search.NewAcceptedAnswerSearch(searchRepo),
notAcceptedQuestion: search.NewNotAcceptedQuestion(searchRepo),
inQuestionSearch: search.NewInQuestionSearch(searchRepo),
questionSearch: search.NewQuestionSearch(searchRepo),
answerSearch: search.NewAnswerSearch(searchRepo),
viewsSearch: search.NewViewsSearch(searchRepo),
objectSearch: search.NewObjectSearch(searchRepo),
searchParser: searchParser,
searchRepo: searchRepo,
}
}
// Search search contents
func (ss *SearchService) Search(ctx context.Context, dto *schema.SearchDTO) (resp []schema.SearchResp, total int64, extra interface{}, err error) {
extra = nil
if dto.Page < 1 {
dto.Page = 1
}
switch {
case ss.tagSearch.Parse(dto):
resp, total, err = ss.tagSearch.Search(ctx)
extra = ss.tagSearch.Extra
case ss.withinSearch.Parse(dto):
resp, total, err = ss.withinSearch.Search(ctx)
case ss.authorSearch.Parse(dto):
resp, total, err = ss.authorSearch.Search(ctx)
case ss.scoreSearch.Parse(dto):
resp, total, err = ss.scoreSearch.Search(ctx)
case ss.answersSearch.Parse(dto):
resp, total, err = ss.answersSearch.Search(ctx)
case ss.acceptedAnswerSearch.Parse(dto):
resp, total, err = ss.acceptedAnswerSearch.Search(ctx)
case ss.notAcceptedQuestion.Parse(dto):
resp, total, err = ss.notAcceptedQuestion.Search(ctx)
case ss.inQuestionSearch.Parse(dto):
resp, total, err = ss.inQuestionSearch.Search(ctx)
case ss.questionSearch.Parse(dto):
resp, total, err = ss.questionSearch.Search(ctx)
case ss.answerSearch.Parse(dto):
resp, total, err = ss.answerSearch.Search(ctx)
case ss.viewsSearch.Parse(dto):
resp, total, err = ss.viewsSearch.Search(ctx)
default:
ss.objectSearch.Parse(dto)
resp, total, err = ss.objectSearch.Search(ctx)
// search type
searchType,
// search all
userID,
votes,
// search questions
notAccepted,
_,
views,
answers,
// search answers
accepted,
questionID,
_,
// common fields
tags,
words := ss.searchParser.ParseStructure(dto)
switch searchType {
case "all":
resp, total, err = ss.searchRepo.SearchContents(ctx, words, tags, userID, votes, dto.Page, dto.Size, dto.Order)
if err != nil {
return nil, 0, nil, err
}
case "question":
resp, total, err = ss.searchRepo.SearchQuestions(ctx, words, notAccepted, views, answers, dto.Page, dto.Size, dto.Order)
case "answer":
resp, total, err = ss.searchRepo.SearchAnswers(ctx, words, tags, accepted, questionID, dto.Page, dto.Size, dto.Order)
}
return resp, total, extra, err
return
}

View File

@ -11,47 +11,107 @@ import (
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/export"
"github.com/answerdev/answer/internal/service/siteinfo_common"
tagcommon "github.com/answerdev/answer/internal/service/tag_common"
"github.com/jinzhu/copier"
"github.com/segmentfault/pacman/errors"
"github.com/segmentfault/pacman/log"
)
type SiteInfoService struct {
siteInfoRepo siteinfo_common.SiteInfoRepo
emailService *export.EmailService
siteInfoRepo siteinfo_common.SiteInfoRepo
emailService *export.EmailService
tagCommonService *tagcommon.TagCommonService
}
func NewSiteInfoService(siteInfoRepo siteinfo_common.SiteInfoRepo, emailService *export.EmailService) *SiteInfoService {
func NewSiteInfoService(
siteInfoRepo siteinfo_common.SiteInfoRepo,
emailService *export.EmailService,
tagCommonService *tagcommon.TagCommonService) *SiteInfoService {
return &SiteInfoService{
siteInfoRepo: siteInfoRepo,
emailService: emailService,
siteInfoRepo: siteInfoRepo,
emailService: emailService,
tagCommonService: tagCommonService,
}
}
// GetSiteGeneral get site info general
func (s *SiteInfoService) GetSiteGeneral(ctx context.Context) (resp *schema.SiteGeneralResp, err error) {
resp = &schema.SiteGeneralResp{}
siteInfo, exist, err := s.siteInfoRepo.GetByType(ctx, constant.SiteTypeGeneral)
if err != nil {
return nil, err
log.Error(err)
return resp, nil
}
if !exist {
return nil, errors.BadRequest(reason.SiteInfoNotFound)
return resp, nil
}
resp = &schema.SiteGeneralResp{}
_ = json.Unmarshal([]byte(siteInfo.Content), resp)
return resp, nil
}
// GetSiteInterface get site info interface
func (s *SiteInfoService) GetSiteInterface(ctx context.Context) (resp *schema.SiteInterfaceResp, err error) {
resp = &schema.SiteInterfaceResp{}
siteInfo, exist, err := s.siteInfoRepo.GetByType(ctx, constant.SiteTypeInterface)
if err != nil {
log.Error(err)
return resp, nil
}
if !exist {
return resp, nil
}
_ = json.Unmarshal([]byte(siteInfo.Content), resp)
return resp, nil
}
// GetSiteBranding get site info branding
func (s *SiteInfoService) GetSiteBranding(ctx context.Context) (resp *schema.SiteBrandingReq, err error) {
resp = &schema.SiteBrandingReq{}
siteInfo, exist, err := s.siteInfoRepo.GetByType(ctx, constant.SiteTypeBranding)
if err != nil {
log.Error(err)
return resp, nil
}
if !exist {
return resp, nil
}
_ = json.Unmarshal([]byte(siteInfo.Content), resp)
return resp, nil
}
// GetSiteWrite get site info write
func (s *SiteInfoService) GetSiteWrite(ctx context.Context) (resp *schema.SiteWriteResp, err error) {
resp = &schema.SiteWriteResp{}
siteInfo, exist, err := s.siteInfoRepo.GetByType(ctx, constant.SiteTypeWrite)
if err != nil {
log.Error(err)
return resp, nil
}
if exist {
_ = json.Unmarshal([]byte(siteInfo.Content), resp)
}
resp.RecommendTags, err = s.tagCommonService.GetSiteWriteRecommendTag(ctx)
if err != nil {
log.Error(err)
}
resp.ReservedTags, err = s.tagCommonService.GetSiteWriteReservedTag(ctx)
if err != nil {
log.Error(err)
}
return resp, nil
}
// GetSiteLegal get site legal info
func (s *SiteInfoService) GetSiteLegal(ctx context.Context) (resp *schema.SiteLegalResp, err error) {
resp = &schema.SiteLegalResp{}
siteInfo, exist, err := s.siteInfoRepo.GetByType(ctx, constant.SiteTypeLegal)
if err != nil {
return nil, err
}
if !exist {
return nil, errors.BadRequest(reason.SiteInfoNotFound)
return resp, nil
}
resp = &schema.SiteInterfaceResp{}
_ = json.Unmarshal([]byte(siteInfo.Content), resp)
return resp, nil
}
@ -109,6 +169,44 @@ func (s *SiteInfoService) SaveSiteInterface(ctx context.Context, req schema.Site
return
}
// SaveSiteBranding save site branding information
func (s *SiteInfoService) SaveSiteBranding(ctx context.Context, req *schema.SiteBrandingReq) (err error) {
content, _ := json.Marshal(req)
data := &entity.SiteInfo{
Type: constant.SiteTypeBranding,
Content: string(content),
Status: 1,
}
return s.siteInfoRepo.SaveByType(ctx, constant.SiteTypeBranding, data)
}
// SaveSiteWrite save site configuration about write
func (s *SiteInfoService) SaveSiteWrite(ctx context.Context, req *schema.SiteWriteReq) (resp interface{}, err error) {
errData, err := s.tagCommonService.SetSiteWriteTag(ctx, req.RecommendTags, req.ReservedTags, req.UserID)
if err != nil {
return errData, err
}
content, _ := json.Marshal(req)
data := &entity.SiteInfo{
Type: constant.SiteTypeWrite,
Content: string(content),
Status: 1,
}
return nil, s.siteInfoRepo.SaveByType(ctx, constant.SiteTypeWrite, data)
}
// SaveSiteLegal save site legal configuration
func (s *SiteInfoService) SaveSiteLegal(ctx context.Context, req *schema.SiteLegalReq) (err error) {
content, _ := json.Marshal(req)
data := &entity.SiteInfo{
Type: constant.SiteTypeLegal,
Content: string(content),
Status: 1,
}
return s.siteInfoRepo.SaveByType(ctx, constant.SiteTypeLegal, data)
}
// GetSMTPConfig get smtp config
func (s *SiteInfoService) GetSMTPConfig(ctx context.Context) (
resp *schema.GetSMTPConfigResp, err error,

View File

@ -5,9 +5,7 @@ import (
"encoding/json"
"github.com/answerdev/answer/internal/base/constant"
"github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/internal/schema"
"github.com/segmentfault/pacman/errors"
)
type SiteInfoCommonService struct {
@ -22,29 +20,70 @@ func NewSiteInfoCommonService(siteInfoRepo SiteInfoRepo) *SiteInfoCommonService
// GetSiteGeneral get site info general
func (s *SiteInfoCommonService) GetSiteGeneral(ctx context.Context) (resp *schema.SiteGeneralResp, err error) {
resp = &schema.SiteGeneralResp{}
siteInfo, exist, err := s.siteInfoRepo.GetByType(ctx, constant.SiteTypeGeneral)
if err != nil {
return nil, err
return resp, err
}
if !exist {
return nil, errors.BadRequest(reason.SiteInfoNotFound)
return resp, nil
}
resp = &schema.SiteGeneralResp{}
_ = json.Unmarshal([]byte(siteInfo.Content), resp)
return resp, nil
}
// GetSiteInterface get site info interface
func (s *SiteInfoCommonService) GetSiteInterface(ctx context.Context) (resp *schema.SiteInterfaceResp, err error) {
resp = &schema.SiteInterfaceResp{}
siteInfo, exist, err := s.siteInfoRepo.GetByType(ctx, constant.SiteTypeInterface)
if err != nil {
return resp, err
}
if !exist {
return resp, nil
}
_ = json.Unmarshal([]byte(siteInfo.Content), resp)
return resp, nil
}
// GetSiteBranding get site info branding
func (s *SiteInfoCommonService) GetSiteBranding(ctx context.Context) (resp *schema.SiteBrandingResp, err error) {
resp = &schema.SiteBrandingResp{}
siteInfo, exist, err := s.siteInfoRepo.GetByType(ctx, constant.SiteTypeBranding)
if err != nil {
return resp, err
}
if !exist {
return resp, nil
}
_ = json.Unmarshal([]byte(siteInfo.Content), resp)
return resp, nil
}
// GetSiteWrite get site info write
func (s *SiteInfoCommonService) GetSiteWrite(ctx context.Context) (resp *schema.SiteWriteResp, err error) {
resp = &schema.SiteWriteResp{}
siteInfo, exist, err := s.siteInfoRepo.GetByType(ctx, constant.SiteTypeWrite)
if err != nil {
return resp, err
}
if !exist {
return resp, nil
}
_ = json.Unmarshal([]byte(siteInfo.Content), resp)
return resp, nil
}
// GetSiteLegal get site info write
func (s *SiteInfoCommonService) GetSiteLegal(ctx context.Context) (resp *schema.SiteLegalResp, err error) {
resp = &schema.SiteLegalResp{}
siteInfo, exist, err := s.siteInfoRepo.GetByType(ctx, constant.SiteTypeLegal)
if err != nil {
return nil, err
}
if !exist {
return nil, errors.BadRequest(reason.SiteInfoNotFound)
return resp, nil
}
resp = &schema.SiteInterfaceResp{}
_ = json.Unmarshal([]byte(siteInfo.Content), resp)
return resp, nil
}

View File

@ -3,7 +3,10 @@ package tag
import (
"context"
"encoding/json"
"github.com/answerdev/answer/internal/service/revision_common"
"github.com/answerdev/answer/internal/service/siteinfo_common"
"github.com/answerdev/answer/internal/service/tag_common"
"github.com/answerdev/answer/pkg/htmltext"
"github.com/answerdev/answer/internal/base/pager"
@ -12,44 +15,44 @@ import (
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/activity_common"
"github.com/answerdev/answer/internal/service/permission"
tagcommon "github.com/answerdev/answer/internal/service/tag_common"
"github.com/answerdev/answer/pkg/converter"
"github.com/jinzhu/copier"
"github.com/segmentfault/pacman/errors"
"github.com/segmentfault/pacman/log"
)
type TagRepo interface {
RemoveTag(ctx context.Context, tagID string) (err error)
UpdateTag(ctx context.Context, tag *entity.Tag) (err error)
UpdateTagSynonym(ctx context.Context, tagSlugNameList []string, mainTagID int64, mainTagSlugName string) (err error)
GetTagList(ctx context.Context, tag *entity.Tag) (tagList []*entity.Tag, err error)
}
// TagService user service
type TagService struct {
tagRepo tagcommon.TagRepo
revisionService *revision_common.RevisionService
followCommon activity_common.FollowRepo
tagRepo TagRepo
tagCommonService *tag_common.TagCommonService
revisionService *revision_common.RevisionService
followCommon activity_common.FollowRepo
siteInfoService *siteinfo_common.SiteInfoCommonService
}
// NewTagService new tag service
func NewTagService(
tagRepo tagcommon.TagRepo,
tagRepo TagRepo,
tagCommonService *tag_common.TagCommonService,
revisionService *revision_common.RevisionService,
followCommon activity_common.FollowRepo) *TagService {
followCommon activity_common.FollowRepo,
siteInfoService *siteinfo_common.SiteInfoCommonService) *TagService {
return &TagService{
tagRepo: tagRepo,
revisionService: revisionService,
followCommon: followCommon,
tagRepo: tagRepo,
tagCommonService: tagCommonService,
revisionService: revisionService,
followCommon: followCommon,
siteInfoService: siteInfoService,
}
}
// SearchTagLike get tag list all
func (ts *TagService) SearchTagLike(ctx context.Context, req *schema.SearchTagLikeReq) (resp []string, err error) {
tags, err := ts.tagRepo.GetTagListByName(ctx, req.Tag, 5)
if err != nil {
return
}
for _, tag := range tags {
resp = append(resp, tag.SlugName)
}
return resp, nil
}
// RemoveTag delete tag
func (ts *TagService) RemoveTag(ctx context.Context, tagID string) (err error) {
// TODO permission
@ -71,7 +74,7 @@ func (ts *TagService) UpdateTag(ctx context.Context, req *schema.UpdateTagReq) (
return err
}
tagInfo, exist, err := ts.tagRepo.GetTagByID(ctx, req.TagID)
tagInfo, exist, err := ts.tagCommonService.GetTagByID(ctx, req.TagID)
if err != nil {
return err
}
@ -116,9 +119,9 @@ func (ts *TagService) GetTagInfo(ctx context.Context, req *schema.GetTagInfoReq)
exist bool
)
if len(req.ID) > 0 {
tagInfo, exist, err = ts.tagRepo.GetTagByID(ctx, req.ID)
tagInfo, exist, err = ts.tagCommonService.GetTagByID(ctx, req.ID)
} else {
tagInfo, exist, err = ts.tagRepo.GetTagBySlugName(ctx, req.Name)
tagInfo, exist, err = ts.tagCommonService.GetTagBySlugName(ctx, req.Name)
}
if err != nil {
return nil, err
@ -130,7 +133,7 @@ func (ts *TagService) GetTagInfo(ctx context.Context, req *schema.GetTagInfoReq)
resp = &schema.GetTagResp{}
// if tag is synonyms get original tag info
if tagInfo.MainTagID > 0 {
tagInfo, exist, err = ts.tagRepo.GetTagByID(ctx, converter.IntToString(tagInfo.MainTagID))
tagInfo, exist, err = ts.tagCommonService.GetTagByID(ctx, converter.IntToString(tagInfo.MainTagID))
if err != nil {
return nil, err
}
@ -148,6 +151,8 @@ func (ts *TagService) GetTagInfo(ctx context.Context, req *schema.GetTagInfoReq)
resp.ParsedText = tagInfo.ParsedText
resp.FollowCount = tagInfo.FollowCount
resp.QuestionCount = tagInfo.QuestionCount
resp.Recommend = tagInfo.Recommend
resp.Reserved = tagInfo.Reserved
resp.IsFollower = ts.checkTagIsFollow(ctx, req.UserID, tagInfo.ID)
resp.MemberActions = permission.GetTagPermission(req.UserID, req.UserID)
resp.GetExcerpt()
@ -165,7 +170,7 @@ func (ts *TagService) GetFollowingTags(ctx context.Context, userID string) (
if err != nil {
return nil, err
}
tagList, err := ts.tagRepo.GetTagListByIDs(ctx, objIDs)
tagList, err := ts.tagCommonService.GetTagListByIDs(ctx, objIDs)
if err != nil {
return nil, err
}
@ -174,9 +179,11 @@ func (ts *TagService) GetFollowingTags(ctx context.Context, userID string) (
TagID: t.ID,
SlugName: t.SlugName,
DisplayName: t.DisplayName,
Recommend: t.Recommend,
Reserved: t.Reserved,
}
if t.MainTagID > 0 {
mainTag, exist, err := ts.tagRepo.GetTagByID(ctx, converter.IntToString(t.MainTagID))
mainTag, exist, err := ts.tagCommonService.GetTagByID(ctx, converter.IntToString(t.MainTagID))
if err != nil {
return nil, err
}
@ -192,7 +199,7 @@ func (ts *TagService) GetFollowingTags(ctx context.Context, userID string) (
// GetTagSynonyms get tag synonyms
func (ts *TagService) GetTagSynonyms(ctx context.Context, req *schema.GetTagSynonymsReq) (
resp []*schema.GetTagSynonymsResp, err error) {
tag, exist, err := ts.tagRepo.GetTagByID(ctx, req.TagID)
tag, exist, err := ts.tagCommonService.GetTagByID(ctx, req.TagID)
if err != nil {
return
}
@ -241,7 +248,7 @@ func (ts *TagService) UpdateTagSynonym(ctx context.Context, req *schema.UpdateTa
req.Format()
addSynonymTagList := make([]string, 0)
removeSynonymTagList := make([]string, 0)
mainTagInfo, exist, err := ts.tagRepo.GetTagByID(ctx, req.TagID)
mainTagInfo, exist, err := ts.tagCommonService.GetTagByID(ctx, req.TagID)
if err != nil {
return err
}
@ -253,7 +260,7 @@ func (ts *TagService) UpdateTagSynonym(ctx context.Context, req *schema.UpdateTa
for _, item := range req.SynonymTagList {
addSynonymTagList = append(addSynonymTagList, item.SlugName)
}
tagListInDB, err := ts.tagRepo.GetTagListByNames(ctx, addSynonymTagList)
tagListInDB, err := ts.tagCommonService.GetTagListByNames(ctx, addSynonymTagList)
if err != nil {
return err
}
@ -278,7 +285,7 @@ func (ts *TagService) UpdateTagSynonym(ctx context.Context, req *schema.UpdateTa
}
if len(needAddTagList) > 0 {
err = ts.tagRepo.AddTagList(ctx, needAddTagList)
err = ts.tagCommonService.AddTagList(ctx, needAddTagList)
if err != nil {
return err
}
@ -336,7 +343,7 @@ func (ts *TagService) GetTagWithPage(ctx context.Context, req *schema.GetTagWith
page := req.Page
pageSize := req.PageSize
tags, total, err := ts.tagRepo.GetTagPage(ctx, page, pageSize, tag, req.QueryCond)
tags, total, err := ts.tagCommonService.GetTagPage(ctx, page, pageSize, tag, req.QueryCond)
if err != nil {
return
}
@ -355,6 +362,8 @@ func (ts *TagService) GetTagWithPage(ctx context.Context, req *schema.GetTagWith
IsFollower: ts.checkTagIsFollow(ctx, req.UserID, tag.ID),
CreatedAt: tag.CreatedAt.Unix(),
UpdatedAt: tag.UpdatedAt.Unix(),
Recommend: tag.Recommend,
Reserved: tag.Reserved,
})
}
return pager.NewPageModel(total, resp), nil

View File

@ -1,30 +1,34 @@
package tagcommon
package tag_common
import (
"context"
"encoding/json"
"fmt"
"sort"
"strings"
"github.com/answerdev/answer/internal/service/revision_common"
"github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/internal/base/validator"
"github.com/answerdev/answer/internal/entity"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/revision_common"
"github.com/answerdev/answer/internal/service/siteinfo_common"
"github.com/segmentfault/pacman/errors"
"github.com/segmentfault/pacman/log"
)
type TagRepo interface {
type TagCommonRepo interface {
AddTagList(ctx context.Context, tagList []*entity.Tag) (err error)
GetTagListByIDs(ctx context.Context, ids []string) (tagList []*entity.Tag, err error)
GetTagBySlugName(ctx context.Context, slugName string) (tagInfo *entity.Tag, exist bool, err error)
GetTagListByName(ctx context.Context, name string, limit int) (tagList []*entity.Tag, err error)
GetTagListByName(ctx context.Context, name string, limit int, hasReserved bool) (tagList []*entity.Tag, err error)
GetTagListByNames(ctx context.Context, names []string) (tagList []*entity.Tag, err error)
RemoveTag(ctx context.Context, tagID string) (err error)
UpdateTag(ctx context.Context, tag *entity.Tag) (err error)
UpdateTagQuestionCount(ctx context.Context, tagID string, questionCount int) (err error)
UpdateTagSynonym(ctx context.Context, tagSlugNameList []string, mainTagID int64, mainTagSlugName string) (err error)
GetTagByID(ctx context.Context, tagID string) (tag *entity.Tag, exist bool, err error)
GetTagList(ctx context.Context, tag *entity.Tag) (tagList []*entity.Tag, err error)
GetTagPage(ctx context.Context, page, pageSize int, tag *entity.Tag, queryCond string) (tagList []*entity.Tag, total int64, err error)
GetRecommendTagList(ctx context.Context) (tagList []*entity.Tag, err error)
GetReservedTagList(ctx context.Context) (tagList []*entity.Tag, err error)
UpdateTagsAttribute(ctx context.Context, tags []string, attribute string, value bool) (err error)
UpdateTagQuestionCount(ctx context.Context, tagID string, questionCount int) (err error)
}
type TagRelRepo interface {
@ -40,39 +44,209 @@ type TagRelRepo interface {
// TagCommonService user service
type TagCommonService struct {
revisionService *revision_common.RevisionService
tagRepo TagRepo
tagCommonRepo TagCommonRepo
tagRelRepo TagRelRepo
siteInfoService *siteinfo_common.SiteInfoCommonService
}
// NewTagCommonService new tag service
func NewTagCommonService(tagRepo TagRepo, tagRelRepo TagRelRepo,
func NewTagCommonService(tagCommonRepo TagCommonRepo, tagRelRepo TagRelRepo,
revisionService *revision_common.RevisionService,
siteInfoService *siteinfo_common.SiteInfoCommonService,
) *TagCommonService {
return &TagCommonService{
tagRepo: tagRepo,
tagCommonRepo: tagCommonRepo,
tagRelRepo: tagRelRepo,
revisionService: revisionService,
siteInfoService: siteInfoService,
}
}
// GetTagListByName
func (ts *TagCommonService) GetTagListByName(ctx context.Context, tagName string) (tagInfo *entity.Tag, exist bool, err error) {
tagName = strings.ToLower(tagName)
return ts.tagRepo.GetTagBySlugName(ctx, tagName)
// SearchTagLike get tag list all
func (ts *TagCommonService) SearchTagLike(ctx context.Context, req *schema.SearchTagLikeReq) (resp []schema.SearchTagLikeResp, err error) {
tags, err := ts.tagCommonRepo.GetTagListByName(ctx, req.Tag, 5, req.IsAdmin)
if err != nil {
return
}
ts.tagsFormatRecommendAndReserved(ctx, tags)
for _, tag := range tags {
item := schema.SearchTagLikeResp{}
item.SlugName = tag.SlugName
item.Recommend = tag.Recommend
item.Reserved = tag.Reserved
resp = append(resp, item)
}
return resp, nil
}
func (ts *TagCommonService) GetSiteWriteRecommendTag(ctx context.Context) (tags []string, err error) {
tags = make([]string, 0)
list, err := ts.tagCommonRepo.GetRecommendTagList(ctx)
if err != nil {
return tags, err
}
for _, item := range list {
tags = append(tags, item.SlugName)
}
return tags, nil
}
func (ts *TagCommonService) SetSiteWriteTag(ctx context.Context, recommendTags, reservedTags []string, userID string) (
errFields []*validator.FormErrorField, err error) {
recommendErr := ts.CheckTag(ctx, recommendTags, userID)
reservedErr := ts.CheckTag(ctx, reservedTags, userID)
if recommendErr != nil {
errFields = append(errFields, &validator.FormErrorField{
ErrorField: "recommend_tags",
ErrorMsg: recommendErr.Error(),
})
err = recommendErr
}
if reservedErr != nil {
errFields = append(errFields, &validator.FormErrorField{
ErrorField: "reserved_tags",
ErrorMsg: reservedErr.Error(),
})
err = reservedErr
}
if len(errFields) > 0 {
return errFields, err
}
err = ts.SetTagsAttribute(ctx, recommendTags, "recommend")
if err != nil {
return nil, err
}
err = ts.SetTagsAttribute(ctx, reservedTags, "reserved")
if err != nil {
return nil, err
}
return nil, nil
}
func (ts *TagCommonService) GetSiteWriteReservedTag(ctx context.Context) (tags []string, err error) {
tags = make([]string, 0)
list, err := ts.tagCommonRepo.GetReservedTagList(ctx)
if err != nil {
return tags, err
}
for _, item := range list {
tags = append(tags, item.SlugName)
}
return tags, nil
}
// SetTagsAttribute
func (ts *TagCommonService) SetTagsAttribute(ctx context.Context, tags []string, attribute string) (err error) {
var tagslist []string
switch attribute {
case "recommend":
tagslist, err = ts.GetSiteWriteRecommendTag(ctx)
case "reserved":
tagslist, err = ts.GetSiteWriteReservedTag(ctx)
default:
return
}
err = ts.tagCommonRepo.UpdateTagsAttribute(ctx, tagslist, attribute, false)
if err != nil {
return err
}
err = ts.tagCommonRepo.UpdateTagsAttribute(ctx, tags, attribute, true)
if err != nil {
return err
}
return nil
}
func (ts *TagCommonService) GetTagListByNames(ctx context.Context, tagNames []string) ([]*entity.Tag, error) {
for k, tagname := range tagNames {
tagNames[k] = strings.ToLower(tagname)
}
return ts.tagRepo.GetTagListByNames(ctx, tagNames)
tagList, err := ts.tagCommonRepo.GetTagListByNames(ctx, tagNames)
if err != nil {
return nil, err
}
ts.tagsFormatRecommendAndReserved(ctx, tagList)
return tagList, nil
}
//
func (ts *TagCommonService) ExistRecommend(ctx context.Context, tags []*schema.TagItem) (bool, error) {
taginfo, err := ts.siteInfoService.GetSiteWrite(ctx)
if err != nil {
return false, err
}
if !taginfo.RequiredTag {
return true, nil
}
tagNames := make([]string, 0)
for _, item := range tags {
tagNames = append(tagNames, item.SlugName)
}
list, err := ts.GetTagListByNames(ctx, tagNames)
if err != nil {
return false, err
}
for _, item := range list {
if item.Recommend {
return true, nil
}
}
return false, nil
}
// GetObjectTag get object tag
func (ts *TagCommonService) GetObjectTag(ctx context.Context, objectId string) (objTags []*schema.TagResp, err error) {
objTags = make([]*schema.TagResp, 0)
tagsInfoList, err := ts.GetObjectEntityTag(ctx, objectId)
return ts.TagFormat(ctx, tagsInfoList)
}
// AddTagList get object tag
func (ts *TagCommonService) AddTagList(ctx context.Context, tagList []*entity.Tag) (err error) {
return ts.tagCommonRepo.AddTagList(ctx, tagList)
}
// GetTagByID get object tag
func (ts *TagCommonService) GetTagByID(ctx context.Context, tagID string) (tag *entity.Tag, exist bool, err error) {
tag, exist, err = ts.tagCommonRepo.GetTagByID(ctx, tagID)
if !exist {
return
}
ts.tagFormatRecommendAndReserved(ctx, tag)
return
}
// GetTagBySlugName get object tag
func (ts *TagCommonService) GetTagBySlugName(ctx context.Context, slugName string) (tag *entity.Tag, exist bool, err error) {
tag, exist, err = ts.tagCommonRepo.GetTagBySlugName(ctx, slugName)
if !exist {
return
}
ts.tagFormatRecommendAndReserved(ctx, tag)
return
}
// GetTagListByIDs get object tag
func (ts *TagCommonService) GetTagListByIDs(ctx context.Context, ids []string) (tagList []*entity.Tag, err error) {
tagList, err = ts.tagCommonRepo.GetTagListByIDs(ctx, ids)
if err != nil {
return nil, err
}
ts.tagsFormatRecommendAndReserved(ctx, tagList)
return
}
// GetTagPage get object tag
func (ts *TagCommonService) GetTagPage(ctx context.Context, page, pageSize int, tag *entity.Tag, queryCond string) (
tagList []*entity.Tag, total int64, err error) {
tagList, total, err = ts.tagCommonRepo.GetTagPage(ctx, page, pageSize, tag, queryCond)
if err != nil {
return nil, 0, err
}
ts.tagsFormatRecommendAndReserved(ctx, tagList)
return
}
func (ts *TagCommonService) GetObjectEntityTag(ctx context.Context, objectId string) (objTags []*entity.Tag, err error) {
tagIDList := make([]string, 0)
tagList, err := ts.tagRelRepo.GetObjectTagRelList(ctx, objectId)
if err != nil {
@ -81,20 +255,57 @@ func (ts *TagCommonService) GetObjectTag(ctx context.Context, objectId string) (
for _, tag := range tagList {
tagIDList = append(tagIDList, tag.TagID)
}
tagsInfoList, err := ts.tagRepo.GetTagListByIDs(ctx, tagIDList)
objTags, err = ts.GetTagListByIDs(ctx, tagIDList)
if err != nil {
return nil, err
}
for _, tagInfo := range tagsInfoList {
return objTags, nil
}
func (ts *TagCommonService) TagFormat(ctx context.Context, tags []*entity.Tag) (objTags []*schema.TagResp, err error) {
objTags = make([]*schema.TagResp, 0)
for _, tagInfo := range tags {
objTags = append(objTags, &schema.TagResp{
SlugName: tagInfo.SlugName,
DisplayName: tagInfo.DisplayName,
MainTagSlugName: tagInfo.MainTagSlugName,
Recommend: tagInfo.Recommend,
Reserved: tagInfo.Reserved,
})
}
return objTags, nil
}
func (ts *TagCommonService) tagsFormatRecommendAndReserved(ctx context.Context, tagList []*entity.Tag) {
if len(tagList) == 0 {
return
}
tagConfig, err := ts.siteInfoService.GetSiteWrite(ctx)
if err != nil {
log.Error(err)
return
}
if !tagConfig.RequiredTag {
for _, tag := range tagList {
tag.Recommend = false
}
}
}
func (ts *TagCommonService) tagFormatRecommendAndReserved(ctx context.Context, tag *entity.Tag) {
if tag == nil {
return
}
tagConfig, err := ts.siteInfoService.GetSiteWrite(ctx)
if err != nil {
log.Error(err)
return
}
if !tagConfig.RequiredTag {
tag.Recommend = false
}
}
// BatchGetObjectTag batch get object tag
func (ts *TagCommonService) BatchGetObjectTag(ctx context.Context, objectIds []string) (map[string][]*schema.TagResp, error) {
objectIDTagMap := make(map[string][]*schema.TagResp)
@ -108,7 +319,7 @@ func (ts *TagCommonService) BatchGetObjectTag(ctx context.Context, objectIds []s
for _, tag := range tagList {
tagIDList = append(tagIDList, tag.TagID)
}
tagsInfoList, err := ts.tagRepo.GetTagListByIDs(ctx, tagIDList)
tagsInfoList, err := ts.GetTagListByIDs(ctx, tagIDList)
if err != nil {
return objectIDTagMap, err
}
@ -123,13 +334,98 @@ func (ts *TagCommonService) BatchGetObjectTag(ctx context.Context, objectIds []s
SlugName: tagInfo.SlugName,
DisplayName: tagInfo.DisplayName,
MainTagSlugName: tagInfo.MainTagSlugName,
Recommend: tagInfo.Recommend,
Reserved: tagInfo.Reserved,
}
objectIDTagMap[item.ObjectID] = append(objectIDTagMap[item.ObjectID], t)
}
}
for _, taglist := range objectIDTagMap {
sort.SliceStable(taglist, func(i, j int) bool {
return taglist[i].Reserved
})
sort.SliceStable(taglist, func(i, j int) bool {
return taglist[i].Recommend
})
}
return objectIDTagMap, nil
}
func (ts *TagCommonService) CheckTag(ctx context.Context, tags []string, userID string) (err error) {
if len(tags) == 0 {
return nil
}
// find tags name
tagListInDb, err := ts.GetTagListByNames(ctx, tags)
if err != nil {
return err
}
tagInDbMapping := make(map[string]*entity.Tag)
checktags := make([]string, 0)
for _, tag := range tagListInDb {
if tag.MainTagID != 0 {
checktags = append(checktags, fmt.Sprintf("\"%s\"", tag.SlugName))
}
tagInDbMapping[tag.SlugName] = tag
}
if len(checktags) > 0 {
err = errors.BadRequest(reason.TagNotContainSynonym).WithMsg(fmt.Sprintf("Should not contain synonym tags %s", strings.Join(checktags, ",")))
return err
}
addTagList := make([]*entity.Tag, 0)
addTagMsgList := make([]string, 0)
for _, tag := range tags {
_, ok := tagInDbMapping[tag]
if ok {
continue
}
item := &entity.Tag{}
item.SlugName = tag
item.DisplayName = tag
item.OriginalText = ""
item.ParsedText = ""
item.Status = entity.TagStatusAvailable
addTagList = append(addTagList, item)
addTagMsgList = append(addTagMsgList, tag)
}
if len(addTagList) > 0 {
err = errors.BadRequest(reason.TagNotFound).WithMsg(fmt.Sprintf("tag [%s] does not exist",
strings.Join(addTagMsgList, ",")))
return err
}
return nil
}
func (ts *TagCommonService) ObjectCheckChangeTag(ctx context.Context, oldobjectTagData, objectTagData []*entity.Tag) (bool, []string) {
reservedTagsMap := make(map[string]bool)
needTagsMap := make([]string, 0)
for _, tag := range objectTagData {
if tag.Reserved {
reservedTagsMap[tag.SlugName] = true
}
}
for _, tag := range oldobjectTagData {
if tag.Reserved {
_, ok := reservedTagsMap[tag.SlugName]
if !ok {
needTagsMap = append(needTagsMap, tag.SlugName)
}
}
}
if len(needTagsMap) > 0 {
return false, needTagsMap
}
return true, []string{}
}
// ObjectChangeTag change object tag list
func (ts *TagCommonService) ObjectChangeTag(ctx context.Context, objectTagData *schema.TagChange) (err error) {
if len(objectTagData.Tags) == 0 {
@ -144,7 +440,7 @@ func (ts *TagCommonService) ObjectChangeTag(ctx context.Context, objectTagData *
}
// find tags name
tagListInDb, err := ts.tagRepo.GetTagListByNames(ctx, thisObjTagNameList)
tagListInDb, err := ts.tagCommonRepo.GetTagListByNames(ctx, thisObjTagNameList)
if err != nil {
return err
}
@ -171,7 +467,7 @@ func (ts *TagCommonService) ObjectChangeTag(ctx context.Context, objectTagData *
}
if len(addTagList) > 0 {
err = ts.tagRepo.AddTagList(ctx, addTagList)
err = ts.tagCommonRepo.AddTagList(ctx, addTagList)
if err != nil {
return err
}
@ -205,7 +501,7 @@ func (ts *TagCommonService) RefreshTagQuestionCount(ctx context.Context, tagIDs
if err != nil {
return err
}
err = ts.tagRepo.UpdateTagQuestionCount(ctx, tagID, int(count))
err = ts.tagCommonRepo.UpdateTagQuestionCount(ctx, tagID, int(count))
if err != nil {
return err
}

View File

@ -4,12 +4,14 @@ import (
"bytes"
"fmt"
"io"
"io/ioutil"
"mime/multipart"
"net/http"
"os"
"path"
"path/filepath"
"strings"
"github.com/answerdev/answer/internal/base/handler"
"github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/internal/service/service_config"
"github.com/answerdev/answer/internal/service/siteinfo_common"
@ -24,6 +26,25 @@ const (
avatarSubPath = "avatar"
avatarThumbSubPath = "avatar_thumb"
postSubPath = "post"
brandingSubPath = "branding"
)
var (
subPathList = []string{
avatarSubPath,
avatarThumbSubPath,
postSubPath,
brandingSubPath,
}
FormatExts = map[string]imaging.Format{
".jpg": imaging.JPEG,
".jpeg": imaging.JPEG,
".png": imaging.PNG,
".gif": imaging.GIF,
".tif": imaging.TIFF,
".tiff": imaging.TIFF,
".bmp": imaging.BMP,
}
)
// UploaderService user service
@ -35,13 +56,11 @@ type UploaderService struct {
// NewUploaderService new upload service
func NewUploaderService(serviceConfig *service_config.ServiceConfig,
siteInfoService *siteinfo_common.SiteInfoCommonService) *UploaderService {
err := dir.CreateDirIfNotExist(filepath.Join(serviceConfig.UploadPath, avatarSubPath))
if err != nil {
panic(err)
}
err = dir.CreateDirIfNotExist(filepath.Join(serviceConfig.UploadPath, postSubPath))
if err != nil {
panic(err)
for _, subPath := range subPathList {
err := dir.CreateDirIfNotExist(filepath.Join(serviceConfig.UploadPath, subPath))
if err != nil {
panic(err)
}
}
return &UploaderService{
serviceConfig: serviceConfig,
@ -49,23 +68,26 @@ func NewUploaderService(serviceConfig *service_config.ServiceConfig,
}
}
func (us *UploaderService) UploadAvatarFile(ctx *gin.Context, file *multipart.FileHeader, fileExt string) (
url string, err error) {
// UploadAvatarFile upload avatar file
func (us *UploaderService) UploadAvatarFile(ctx *gin.Context) (url string, err error) {
// max size
ctx.Request.Body = http.MaxBytesReader(ctx.Writer, ctx.Request.Body, 5*1024*1024)
_, file, err := ctx.Request.FormFile("file")
if err != nil {
handler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), nil)
return
}
fileExt := strings.ToLower(path.Ext(file.Filename))
if _, ok := FormatExts[fileExt]; !ok {
handler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), nil)
return
}
newFilename := fmt.Sprintf("%s%s", uid.IDStr12(), fileExt)
avatarFilePath := path.Join(avatarSubPath, newFilename)
return us.uploadFile(ctx, file, avatarFilePath)
}
var FormatExts = map[string]imaging.Format{
".jpg": imaging.JPEG,
".jpeg": imaging.JPEG,
".png": imaging.PNG,
".gif": imaging.GIF,
".tif": imaging.TIFF,
".tiff": imaging.TIFF,
".bmp": imaging.BMP,
}
func (us *UploaderService) AvatarThumbFile(ctx *gin.Context, uploadPath, fileName string, size int) (
avatarfile []byte, err error) {
if size > 1024 {
@ -73,12 +95,12 @@ func (us *UploaderService) AvatarThumbFile(ctx *gin.Context, uploadPath, fileNam
}
thumbFileName := fmt.Sprintf("%d_%d@%s", size, size, fileName)
thumbfilePath := fmt.Sprintf("%s/%s/%s", uploadPath, avatarThumbSubPath, thumbFileName)
avatarfile, err = ioutil.ReadFile(thumbfilePath)
avatarfile, err = os.ReadFile(thumbfilePath)
if err == nil {
return avatarfile, nil
}
filePath := fmt.Sprintf("%s/avatar/%s", uploadPath, fileName)
avatarfile, err = ioutil.ReadFile(filePath)
avatarfile, err = os.ReadFile(filePath)
if err != nil {
return avatarfile, errors.InternalServer(reason.UnknownError).WithError(err).WithStack()
}
@ -117,13 +139,47 @@ func (us *UploaderService) AvatarThumbFile(ctx *gin.Context, uploadPath, fileNam
return buf.Bytes(), nil
}
func (us *UploaderService) UploadPostFile(ctx *gin.Context, file *multipart.FileHeader, fileExt string) (
func (us *UploaderService) UploadPostFile(ctx *gin.Context) (
url string, err error) {
// max size
ctx.Request.Body = http.MaxBytesReader(ctx.Writer, ctx.Request.Body, 10*1024*1024)
_, file, err := ctx.Request.FormFile("file")
if err != nil {
handler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), nil)
return
}
fileExt := strings.ToLower(path.Ext(file.Filename))
if _, ok := FormatExts[fileExt]; !ok {
handler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), nil)
return
}
newFilename := fmt.Sprintf("%s%s", uid.IDStr12(), fileExt)
avatarFilePath := path.Join(postSubPath, newFilename)
return us.uploadFile(ctx, file, avatarFilePath)
}
func (us *UploaderService) UploadBrandingFile(ctx *gin.Context) (
url string, err error) {
// max size
ctx.Request.Body = http.MaxBytesReader(ctx.Writer, ctx.Request.Body, 10*1024*1024)
_, file, err := ctx.Request.FormFile("file")
if err != nil {
handler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), nil)
return
}
fileExt := strings.ToLower(path.Ext(file.Filename))
_, ok := FormatExts[fileExt]
if !ok && fileExt != ".ico" {
handler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), nil)
return
}
newFilename := fmt.Sprintf("%s%s", uid.IDStr12(), fileExt)
avatarFilePath := path.Join(brandingSubPath, newFilename)
return us.uploadFile(ctx, file, avatarFilePath)
}
func (us *UploaderService) uploadFile(ctx *gin.Context, file *multipart.FileHeader, fileSubPath string) (
url string, err error) {
siteGeneral, err := us.siteInfoService.GetSiteGeneral(ctx)

View File

@ -12,6 +12,7 @@ import (
"github.com/Chain-Zhang/pinyin"
"github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/internal/base/translator"
"github.com/answerdev/answer/internal/base/validator"
"github.com/answerdev/answer/internal/entity"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/activity"
@ -112,6 +113,7 @@ func (us *UserService) EmailLogin(ctx context.Context, req *schema.UserEmailLogi
UserID: userInfo.ID,
EmailStatus: userInfo.MailStatus,
UserStatus: userInfo.Status,
IsAdmin: userInfo.IsAdmin,
}
resp.AccessToken, err = us.authService.SetUserCacheInfo(ctx, userCacheInfo)
if err != nil {
@ -322,6 +324,7 @@ func (us *UserService) UserRegisterByEmail(ctx context.Context, registerUserInfo
UserID: userInfo.ID,
EmailStatus: userInfo.MailStatus,
UserStatus: userInfo.Status,
IsAdmin: userInfo.IsAdmin,
}
resp.AccessToken, err = us.authService.SetUserCacheInfo(ctx, userCacheInfo)
if err != nil {
@ -408,11 +411,16 @@ func (us *UserService) UserVerifyEmail(ctx context.Context, req *schema.UserVeri
UserID: userInfo.ID,
EmailStatus: userInfo.MailStatus,
UserStatus: userInfo.Status,
IsAdmin: userInfo.IsAdmin,
}
resp.AccessToken, err = us.authService.SetUserCacheInfo(ctx, userCacheInfo)
if err != nil {
return nil, err
}
// User verified email will update user email status. So user status cache should be updated.
if err = us.authService.SetUserStatus(ctx, userCacheInfo); err != nil {
return nil, err
}
resp.IsAdmin = userInfo.IsAdmin
if resp.IsAdmin {
err = us.authService.SetCmsUserCacheInfo(ctx, resp.AccessToken, &entity.UserCacheInfo{UserID: userInfo.ID})
@ -478,7 +486,7 @@ func (us *UserService) encryptPassword(ctx context.Context, Pass string) (string
// UserChangeEmailSendCode user change email verification
func (us *UserService) UserChangeEmailSendCode(ctx context.Context, req *schema.UserChangeEmailSendCodeReq) (
resp *schema.UserVerifyEmailErrorResponse, err error) {
resp *validator.FormErrorField, err error) {
userInfo, exist, err := us.userRepo.GetByUserID(ctx, req.UserID)
if err != nil {
return nil, err
@ -492,9 +500,9 @@ func (us *UserService) UserChangeEmailSendCode(ctx context.Context, req *schema.
return nil, err
}
if exist {
resp = &schema.UserVerifyEmailErrorResponse{
Key: "e_mail",
Value: reason.EmailDuplicate,
resp = &validator.FormErrorField{
ErrorField: "e_mail",
ErrorMsg: reason.EmailDuplicate,
}
return resp, errors.BadRequest(reason.EmailDuplicate)
}

View File

@ -35,6 +35,7 @@
"react-helmet-async": "^1.3.0",
"react-i18next": "^11.18.3",
"react-router-dom": "^6.4.0",
"semver": "^7.3.8",
"swr": "^1.3.0",
"zustand": "^4.1.1"
},

View File

@ -60,6 +60,7 @@ specifiers:
react-router-dom: ^6.4.0
react-scripts: 5.0.1
sass: ^1.54.4
semver: ^7.3.8
swr: ^1.3.0
typescript: ^4.8.3
yaml-loader: ^0.8.0
@ -88,6 +89,7 @@ dependencies:
react-helmet-async: 1.3.0_biqbaboplfbrettd7655fr4n2y
react-i18next: 11.18.6_ulhmqqxshznzmtuvahdi5nasbq
react-router-dom: 6.4.0_biqbaboplfbrettd7655fr4n2y
semver: 7.3.8
swr: 1.3.0_react@18.2.0
zustand: 4.1.1_react@18.2.0
@ -1489,7 +1491,7 @@ packages:
cosmiconfig-typescript-loader: 4.1.0_3owiowz3ujipd4k6pbqn3n7oui
lodash: 4.17.21
resolve-from: 5.0.0
ts-node: 10.9.1_ao52im6kiihokc7tdj7weudhra
ts-node: 10.9.1_ck2axrxkiif44rdbzjywaqjysa
typescript: 4.8.3
transitivePeerDependencies:
- '@swc/core'
@ -2670,7 +2672,7 @@ packages:
eslint: 8.23.1
ignore: 5.2.0
regexpp: 3.2.0
semver: 7.3.7
semver: 7.3.8
tsutils: 3.21.0_typescript@4.8.3
typescript: 4.8.3
transitivePeerDependencies:
@ -2751,7 +2753,7 @@ packages:
debug: 4.3.4
globby: 11.1.0
is-glob: 4.0.3
semver: 7.3.7
semver: 7.3.8
tsutils: 3.21.0_typescript@4.8.3
typescript: 4.8.3
transitivePeerDependencies:
@ -3475,7 +3477,7 @@ packages:
/builtins/5.0.1:
resolution: {integrity: sha512-qwVpFEHNfhYJIzNRBvd2C1kyo6jz3ZSMPyyuR47OPdiKWlbYnZNyDWuyR175qDnAJLiCo5fBBqPb3RiXgWlkOQ==}
dependencies:
semver: 7.3.7
semver: 7.3.8
dev: true
/bytes/3.0.0:
@ -3833,7 +3835,7 @@ packages:
dependencies:
'@types/node': 14.18.29
cosmiconfig: 7.0.1
ts-node: 10.9.1_ao52im6kiihokc7tdj7weudhra
ts-node: 10.9.1_ck2axrxkiif44rdbzjywaqjysa
typescript: 4.8.3
dev: true
@ -3917,7 +3919,7 @@ packages:
postcss-modules-scope: 3.0.0_postcss@8.4.16
postcss-modules-values: 4.0.0_postcss@8.4.16
postcss-value-parser: 4.2.0
semver: 7.3.7
semver: 7.3.8
webpack: 5.74.0
/css-minimizer-webpack-plugin/3.4.1_webpack@5.74.0:
@ -5289,7 +5291,7 @@ packages:
is-core-module: 2.10.0
minimatch: 3.1.2
resolve: 1.22.1
semver: 7.3.7
semver: 7.3.8
dev: true
/eslint-plugin-prettier/4.2.1_cabrci5exjdaojcvd6xoxgeowu:
@ -5753,7 +5755,7 @@ packages:
memfs: 3.4.7
minimatch: 3.1.2
schema-utils: 2.7.0
semver: 7.3.7
semver: 7.3.8
tapable: 1.1.3
typescript: 4.8.3
webpack: 5.74.0
@ -6925,7 +6927,7 @@ packages:
jest-util: 27.5.1
natural-compare: 1.4.0
pretty-format: 27.5.1
semver: 7.3.7
semver: 7.3.8
transitivePeerDependencies:
- supports-color
@ -7632,7 +7634,7 @@ packages:
dependencies:
hosted-git-info: 4.1.0
is-core-module: 2.10.0
semver: 7.3.7
semver: 7.3.8
validate-npm-package-license: 3.0.4
dev: true
@ -8236,7 +8238,7 @@ packages:
cosmiconfig: 7.0.1
klona: 2.0.5
postcss: 8.4.16
semver: 7.3.7
semver: 7.3.8
webpack: 5.74.0
/postcss-logical/5.0.4_postcss@8.4.16:
@ -9061,7 +9063,7 @@ packages:
resolve: 1.22.1
resolve-url-loader: 4.0.0
sass-loader: 12.6.0_sass@1.54.9+webpack@5.74.0
semver: 7.3.7
semver: 7.3.8
source-map-loader: 3.0.1_webpack@5.74.0
style-loader: 3.3.1_webpack@5.74.0
tailwindcss: 3.1.8_57znarxsqwmnneadci5z5fd5gu
@ -9508,6 +9510,14 @@ packages:
hasBin: true
dependencies:
lru-cache: 6.0.0
dev: true
/semver/7.3.8:
resolution: {integrity: sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==}
engines: {node: '>=10'}
hasBin: true
dependencies:
lru-cache: 6.0.0
/send/0.18.0:
resolution: {integrity: sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==}
@ -10203,6 +10213,37 @@ packages:
v8-compile-cache-lib: 3.0.1
yn: 3.1.1
/ts-node/10.9.1_ck2axrxkiif44rdbzjywaqjysa:
resolution: {integrity: sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==}
hasBin: true
peerDependencies:
'@swc/core': '>=1.2.50'
'@swc/wasm': '>=1.2.50'
'@types/node': '*'
typescript: '>=2.7'
peerDependenciesMeta:
'@swc/core':
optional: true
'@swc/wasm':
optional: true
dependencies:
'@cspotcode/source-map-support': 0.8.1
'@tsconfig/node10': 1.0.9
'@tsconfig/node12': 1.0.11
'@tsconfig/node14': 1.0.3
'@tsconfig/node16': 1.0.3
'@types/node': 14.18.29
acorn: 8.8.0
acorn-walk: 8.2.0
arg: 4.1.3
create-require: 1.1.1
diff: 4.0.2
make-error: 1.3.6
typescript: 4.8.3
v8-compile-cache-lib: 3.0.1
yn: 3.1.1
dev: true
/tsconfig-paths/3.14.1:
resolution: {integrity: sha512-fxDhWnFSLt3VuTwtvJt5fpwxBHg5AdKWMsgcPOOIilyjymcYVZoCQF8fvFRezCNfblEXmi+PcM1eYHeOAgXCOQ==}
dependencies:

View File

@ -69,6 +69,7 @@ export interface AnswerParams {
html: string;
question_id: string;
id: string;
edit_summary?: string;
}
export interface LoginReqParams {

View File

@ -229,12 +229,7 @@ const TagSelector: FC<IProps> = ({
{showRequiredTagText &&
tags &&
tags.filter((v) => v.recommend)?.length > 0 && (
<Dropdown.Item
disabled
style={{ fontWeight: 500 }}
className="text-secondary">
{t('tag_required_text')}
</Dropdown.Item>
<h6 className="dropdown-header">{t('tag_required_text')}</h6>
)}
{tags?.map((item, index) => {

View File

@ -66,7 +66,7 @@ a {
display: inline-block;
font-size: 14px;
background: rgba($blue-100, 0.5);
padding: 1px 7px;
padding: 0px 7px 1px;
color: $blue-700;
border: 1px solid transparent;
&:hover {

View File

@ -5,6 +5,8 @@ import { Link } from 'react-router-dom';
import type * as Type from '@/common/interface';
const { gt, gte } = require('semver');
interface IProps {
data: Type.AdminDashboard['info'];
}
@ -12,7 +14,12 @@ interface IProps {
const HealthStatus: FC<IProps> = ({ data }) => {
const { t } = useTranslation('translation', { keyPrefix: 'admin.dashboard' });
const { version, remote_version } = data.version_info || {};
const isLatest = version === remote_version;
let isLatest = false;
let hasNewerVersion = false;
if (version && remote_version) {
isLatest = gte(version, remote_version);
hasNewerVersion = gt(remote_version, version);
}
return (
<Card className="mb-4">
<Card.Body>
@ -30,7 +37,7 @@ const HealthStatus: FC<IProps> = ({ data }) => {
{t('latest')}
</a>
)}
{!isLatest && remote_version && (
{!isLatest && hasNewerVersion && (
<a
className="ms-1 badge rounded-pill text-bg-warning"
target="_blank"

View File

@ -75,19 +75,6 @@ const Interface: FC = () => {
},
});
// const onChange = (fieldName, fieldValue) => {
// if (!formData[fieldName]) {
// return;
// }
// const fieldData: FormDataType = {
// [fieldName]: {
// value: fieldValue,
// isInvalid: false,
// errorMsg: '',
// },
// };
// setFormData({ ...formData, ...fieldData });
// };
const uiSchema: UISchema = {
theme: {
'ui:widget': 'select',
@ -211,119 +198,6 @@ const Interface: FC = () => {
onSubmit={onSubmit}
onChange={handleOnChange}
/>
{/* <Form noValidate onSubmit={onSubmit}>
<Form.Group controlId="logo" className="mb-3">
<Form.Label>{t('logo.label')}</Form.Label>
<Stack gap={2}>
<div
className="bg-light overflow-hidden"
style={{ width: '288px', height: '96px' }}>
{formData.logo.value ? (
<Image
width="288"
height="96"
className="object-fit-contain"
src={formData.logo.value}
/>
) : null}
</div>
<div className="d-inline-flex">
<UploadImg type="logo" upload={imgUpload} className="mb-2" />
</div>
</Stack>
<Form.Text as="div" className="text-muted">
<Trans i18nKey="admin.interface.logo.text">
You can upload your image or
<Button
variant="link"
size="sm"
className="p-0 mx-1"
onClick={(evt) => {
evt.preventDefault();
onChange('logo', '');
}}>
reset it
</Button>
to the site title text.
</Trans>
</Form.Text>
<Form.Control.Feedback type="invalid">
{formData.logo.errorMsg}
</Form.Control.Feedback>
</Form.Group>
<Form.Group controlId="theme" className="mb-3">
<Form.Label>{t('theme.label')}</Form.Label>
<Form.Select
value={formData.theme.value}
isInvalid={formData.theme.isInvalid}
onChange={(evt) => {
onChange('theme', evt.target.value);
}}>
{themes?.map((item) => {
return (
<option value={item.value} key={item.value}>
{item.label}
</option>
);
})}
</Form.Select>
<Form.Text as="div">{t('theme.text')}</Form.Text>
<Form.Control.Feedback type="invalid">
{formData.theme.errorMsg}
</Form.Control.Feedback>
</Form.Group>
<Form.Group controlId="language" className="mb-3">
<Form.Label>{t('language.label')}</Form.Label>
<Form.Select
value={formData.language.value}
isInvalid={formData.language.isInvalid}
onChange={(evt) => {
onChange('language', evt.target.value);
}}>
{langs?.map((item) => {
return (
<option value={item.value} key={item.value}>
{item.label}
</option>
);
})}
</Form.Select>
<Form.Text as="div">{t('language.text')}</Form.Text>
<Form.Control.Feedback type="invalid">
{formData.language.errorMsg}
</Form.Control.Feedback>
</Form.Group>
<Form.Group controlId="time-zone" className="mb-3">
<Form.Label>{t('time_zone.label')}</Form.Label>
<Form.Select
value={formData.time_zone.value}
isInvalid={formData.time_zone.isInvalid}
onChange={(evt) => {
onChange('time_zone', evt.target.value);
}}>
{TIMEZONES?.map((item) => {
return (
<optgroup label={item.label} key={item.label}>
{item.options.map((option) => {
return (
<option value={option.value} key={option.value}>
{option.label}
</option>
);
})}
</optgroup>
);
})}
</Form.Select>
<Form.Text as="div">{t('time_zone.text')}</Form.Text>
<Form.Control.Feedback type="invalid">
{formData.time_zone.errorMsg}
</Form.Control.Feedback>
</Form.Group>
<Button variant="primary" type="submit">
{t('save', { keyPrefix: 'btns' })}
</Button>
</Form> */}
</>
);
};

View File

@ -78,11 +78,13 @@ const Legal: FC = () => {
useEffect(() => {
getLegalSetting().then((setting) => {
const formMeta = { ...formData };
formMeta.terms_of_service.value = setting.terms_of_service_original_text;
formMeta.privacy_policy.value = setting.privacy_policy_original_text;
setFormData(formMeta);
if (setting) {
const formMeta = { ...formData };
formMeta.terms_of_service.value =
setting.terms_of_service_original_text;
formMeta.privacy_policy.value = setting.privacy_policy_original_text;
setFormData(formMeta);
}
});
}, []);

View File

@ -29,7 +29,8 @@ const Legal: FC = () => {
},
required_tag: {
type: 'boolean',
title: t('required_tag.label'),
title: t('required_tag.title'),
label: t('required_tag.label'),
description: t('required_tag.text'),
},
reserved_tags: {
@ -43,7 +44,7 @@ const Legal: FC = () => {
recommend_tags: {
'ui:widget': 'textarea',
'ui:options': {
rows: 5,
rows: 10,
},
},
required_tag: {
@ -52,7 +53,7 @@ const Legal: FC = () => {
reserved_tags: {
'ui:widget': 'textarea',
'ui:options': {
rows: 5,
rows: 10,
},
},
};
@ -61,14 +62,19 @@ const Legal: FC = () => {
const onSubmit = (evt) => {
evt.preventDefault();
evt.stopPropagation();
let recommend_tags = [];
if (formData.recommend_tags.value?.trim()) {
recommend_tags = formData.recommend_tags.value.trim().split('\n');
}
let reserved_tags = [];
if (formData.reserved_tags.value?.trim()) {
reserved_tags = formData.reserved_tags.value.trim().split('\n');
}
const reqParams: Type.AdminSettingsWrite = {
recommend_tags: formData.recommend_tags.value.trim().split('\n'),
recommend_tags,
reserved_tags,
required_tag: formData.required_tag.value,
reserved_tags: formData.reserved_tags.value.trim().split('\n'),
};
console.log(reqParams);
postRequireAndReservedTag(reqParams)
.then(() => {
Toast.onShow({
@ -86,9 +92,13 @@ const Legal: FC = () => {
const initData = () => {
getRequireAndReservedTag().then((res) => {
formData.recommend_tags.value = res.recommend_tags.join('\n');
if (Array.isArray(res.recommend_tags)) {
formData.recommend_tags.value = res.recommend_tags.join('\n');
}
formData.required_tag.value = res.required_tag;
formData.reserved_tags.value = res.reserved_tags.join('\n');
if (Array.isArray(res.reserved_tags)) {
formData.reserved_tags.value = res.reserved_tags.join('\n');
}
setFormData({ ...formData });
});
};

View File

@ -23,7 +23,7 @@ const Index: FC = () => {
return (
<>
<PageTitle title={t('privacy')} />
<h3>{t('privacy')}</h3>
<h3 className="mb-4">{t('privacy')}</h3>
<div
className="fmt"
dangerouslySetInnerHTML={{

View File

@ -22,7 +22,7 @@ const Index: FC = () => {
return (
<>
<PageTitle title={t('tos')} />
<h3>{t('tos')}</h3>
<h3 className="mb-4">{t('tos')}</h3>
<div
className="fmt"
dangerouslySetInnerHTML={{

View File

@ -1,4 +0,0 @@
.sub-container {
padding-top: 2rem;
padding-bottom: 2rem;
}

View File

@ -1,20 +1,27 @@
import { FC } from 'react';
import { Container, Row, Col } from 'react-bootstrap';
import { Outlet } from 'react-router-dom';
import { AccordionNav } from '@/components';
import { ADMIN_LEGAL_MENUS } from '@/common/constants';
import './index.scss';
import { Container, Row, Col, Nav } from 'react-bootstrap';
import { Outlet, NavLink } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
const Index: FC = () => {
const { t } = useTranslation('translation', { keyPrefix: 'nav_menus' });
return (
<Container className="sub-container">
<Row>
<Col lg={2}>
<AccordionNav menus={ADMIN_LEGAL_MENUS} />
<Container className="pt-4 mt-2 mb-5">
<Row className="justify-content-center">
<Col xxl={10}>
<Nav
className="mb-4 flex-nowrap"
variant="pills"
style={{ overflow: 'auto' }}>
<NavLink to="/tos" key="tos" className="nav-link">
{t('tos')}
</NavLink>
<NavLink to="/privacy" key="privacy" className="nav-link">
{t('privacy')}
</NavLink>
</Nav>
</Col>
<Col lg={6}>
<Col xxl={10}>
<Outlet />
</Col>
</Row>

View File

@ -60,6 +60,11 @@ const Ask = () => {
const [formData, setFormData] = useState<FormDataItem>(initFormData);
const [checked, setCheckState] = useState(false);
const [focusType, setForceType] = useState('');
const resetForm = () => {
setFormData(initFormData);
setCheckState(false);
setForceType('');
};
const editorRef = useRef<EditorRef>({
getHtml: () => '',
@ -75,7 +80,11 @@ const Ask = () => {
const { data: similarQuestions = { list: [] } } = useQueryQuestionByTitle(
isEdit ? '' : formData.title.value,
);
useEffect(() => {
if (!isEdit) {
resetForm();
}
}, [isEdit]);
const { data: revisions = [] } = useQueryRevisions(qid);
useEffect(() => {

View File

@ -1,5 +1,5 @@
.edit-answer-wrap {
.content-wrap {
.question-content-wrap {
position: relative;
margin-bottom: 20px;
overflow: hidden;

View File

@ -62,6 +62,13 @@ const Ask = () => {
...formData,
answer: { ...formData.answer, value },
});
const handleSummaryChange = (evt) => {
const v = evt.currentTarget.value;
setFormData({
...formData,
description: { ...formData.description, value: v },
});
};
const checkValidated = (): boolean => {
let bol = true;
@ -100,6 +107,7 @@ const Ask = () => {
html: editorRef.current.getHtml(),
question_id: qid,
id: aid,
edit_summary: formData.description.value,
};
modifyAnswer(params).then(() => {
navigate(`/questions/${qid}/${aid}`);
@ -132,7 +140,7 @@ const Ask = () => {
<h5 className="mb-3">{data?.question.title}</h5>
</a>
<div className="content-wrap">
<div className="question-content-wrap">
<div
ref={questionContentRef}
className="content position-absolute top-0 w-100"
@ -166,7 +174,7 @@ const Ask = () => {
</Form.Select>
</Form.Group>
<Form.Group controlId="answer" className="mt-4">
<Form.Group controlId="answer" className="mt-3">
<Form.Label>{t('form.fields.answer.label')}</Form.Label>
<Editor
value={formData.answer.value}
@ -198,6 +206,7 @@ const Ask = () => {
<Form.Label>{t('form.fields.edit_summary.label')}</Form.Label>
<Form.Control
type="text"
onChange={handleSummaryChange}
defaultValue={formData.description.value}
isInvalid={formData.description.isInvalid}
placeholder={t('form.fields.edit_summary.placeholder')}

View File

@ -3,8 +3,14 @@ import { Container, Row, Col, Alert, Stack, Button } from 'react-bootstrap';
import { Link } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { BaseUserCard, FormatTime, Empty, DiffContent } from '@/components';
import { useReviewList, revisionAudit } from '@/services';
import {
BaseUserCard,
FormatTime,
Empty,
DiffContent,
PageTitle,
} from '@/components';
import { getReviewList, revisionAudit } from '@/services';
import { pathFactory } from '@/router/pathFactory';
import type * as Type from '@/common/interface';
@ -13,24 +19,46 @@ const Index: FC = () => {
const [isLoading, setIsLoading] = useState(false);
const [noTasks, setNoTasks] = useState(false);
const [page, setPage] = useState(1);
const { data: reviewResp, mutate: mutateList } = useReviewList(page);
const [reviewResp, setReviewResp] = useState<Type.ReviewResp>();
const ro = reviewResp?.list[0];
const { info, type, unreviewed_info } = ro || {
info: null,
type: '',
unreviewed_info: null,
};
const reviewInfo = unreviewed_info?.content;
const mutateNextPage = () => {
const count = reviewResp?.count;
if (count && page < count) {
setPage(page + 1);
} else {
const resolveNextOne = (resp, pageNumber) => {
const { count, list = [] } = resp;
// auto rollback
if (!list.length && count && page !== 1) {
pageNumber = 1;
setPage(pageNumber);
// eslint-disable-next-line @typescript-eslint/no-use-before-define
queryNextOne(pageNumber);
return;
}
if (pageNumber !== page) {
setPage(pageNumber);
}
setReviewResp(resp);
if (!list.length) {
setNoTasks(true);
}
setTimeout(() => {
window.scrollTo({ top: 0 });
}, 150);
};
const queryNextOne = (pageNumber) => {
getReviewList(pageNumber)
.then((resp) => {
resolveNextOne(resp, pageNumber);
})
.catch((ex) => {
console.log('ex: ', ex);
});
};
const reviewInfo = unreviewed_info?.content;
const handlingSkip = () => {
mutateNextPage();
queryNextOne(page + 1);
};
const handlingApprove = () => {
if (!unreviewed_info) {
@ -39,7 +67,7 @@ const Index: FC = () => {
setIsLoading(true);
revisionAudit(unreviewed_info.id, 'approve')
.then(() => {
mutateList();
queryNextOne(page);
})
.catch((ex) => {
console.log('ex: ', ex);
@ -55,7 +83,7 @@ const Index: FC = () => {
setIsLoading(true);
revisionAudit(unreviewed_info.id, 'reject')
.then(() => {
mutateList();
queryNextOne(page);
})
.catch((ex) => {
console.log('ex: ', ex);
@ -93,16 +121,11 @@ const Index: FC = () => {
editSummary ||= t('edit_tag');
}
useEffect(() => {
if (!reviewResp) {
return;
}
window.scrollTo({ top: 0 });
if (!reviewResp.list || !reviewResp.list.length) {
setNoTasks(true);
}
}, [reviewResp]);
queryNextOne(page);
}, []);
return (
<Container className="pt-2 mt-4 mb-5">
<PageTitle title={t('review')} />
<Row>
<Col lg={{ span: 7, offset: 1 }}>
<h3 className="mb-4">{t('review')}</h3>

View File

@ -73,7 +73,9 @@ const Index: FC = () => {
<Row className="py-3 justify-content-center">
<Col xxl={10}>
<h5 className="mb-4">
{t('title')}{' '}
{timelineData?.object_info.object_type === 'tag'
? t('tag_title')
: t('title')}{' '}
<Link to={linkUrl}>{timelineData?.object_info?.title}</Link>
</h5>
{timelineData?.object_info.object_type !== 'tag' && (

View File

@ -1,5 +1,3 @@
import useSWR from 'swr';
import request from '@/utils/request';
import * as Type from '@/common/interface';
@ -16,16 +14,7 @@ export const revisionAudit = (id: string, operation: 'approve' | 'reject') => {
});
};
export const useReviewList = (page: number) => {
export const getReviewList = (page: number) => {
const apiUrl = `/answer/api/v1/revisions/unreviewed?page=${page}`;
const { data, error, mutate } = useSWR<Type.ReviewResp, Error>(
apiUrl,
request.instance.get,
);
return {
data,
isLoading: !data && !error,
error,
mutate,
};
return request.get<Type.ReviewResp>(apiUrl);
};