Merge branch 'dev' into feat/ui-0.6.0

This commit is contained in:
haitao(lj) 2022-12-06 17:30:01 +08:00
commit fcb6197748
108 changed files with 4639 additions and 1062 deletions

View File

@ -2,7 +2,7 @@
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
labels: bug
assignees: ''
---
@ -23,16 +23,8 @@ 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]
**Platform (please complete the following information):**
- Device: [e.g. Desktop, Mobile]
- OS: [e.g. macOS]
- 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

@ -2,7 +2,7 @@
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
labels: enhancement
assignees: ''
---
@ -15,6 +15,3 @@ 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

@ -38,6 +38,8 @@ stages:
"build docker images and push":
stage: push
before_script:
- export GOPROXY=https://goproxy.cn,direct
extends: .docker-build-push
only:
- test

1
.husky/.gitignore vendored
View File

@ -1 +0,0 @@
_

View File

@ -1,4 +0,0 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
cd ui && pnpm commitlint --edit $1 --config commitlint.config.js

View File

@ -1,4 +0,0 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
cd ui && npm run pre-commit

View File

@ -1,6 +1,6 @@
.PHONY: build clean ui
VERSION=0.4.0
VERSION=0.5.0
BIN=answer
DIR_SRC=./cmd/answer
DOCKER_CMD=docker
@ -38,7 +38,6 @@ install-ui-packages:
@corepack prepare pnpm@v7.12.2 --activate
ui:
@npm config set registry https://repo.huaweicloud.com/repository/npm/
@cd ui && pnpm install && pnpm build && cd -
all: clean build

View File

@ -35,12 +35,12 @@ var (
// @in header
// @name Authorization
func main() {
log.SetLogger(zap.NewLogger(
log.ParseLevel(logLevel), zap.WithName("answer"), zap.WithPath(logPath), zap.WithCallerFullPath()))
Execute()
}
func runApp() {
log.SetLogger(zap.NewLogger(
log.ParseLevel(logLevel), zap.WithName("answer"), zap.WithPath(logPath), zap.WithCallerFullPath()))
c, err := conf.ReadConfig(cli.GetConfigFilePath())
if err != nil {
panic(err)

View File

@ -41,10 +41,12 @@ import (
"github.com/answerdev/answer/internal/service"
"github.com/answerdev/answer/internal/service/action"
activity2 "github.com/answerdev/answer/internal/service/activity"
activity_common2 "github.com/answerdev/answer/internal/service/activity_common"
"github.com/answerdev/answer/internal/service/answer_common"
auth2 "github.com/answerdev/answer/internal/service/auth"
"github.com/answerdev/answer/internal/service/collection_common"
comment2 "github.com/answerdev/answer/internal/service/comment"
"github.com/answerdev/answer/internal/service/comment_common"
"github.com/answerdev/answer/internal/service/dashboard"
export2 "github.com/answerdev/answer/internal/service/export"
"github.com/answerdev/answer/internal/service/follow"
@ -118,7 +120,12 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database,
answerRepo := answer.NewAnswerRepo(dataData, uniqueIDRepo, userRankRepo, activityRepo)
questionRepo := question.NewQuestionRepo(dataData, uniqueIDRepo)
tagCommonRepo := tag_common.NewTagCommonRepo(dataData, uniqueIDRepo)
objService := object_info.NewObjService(answerRepo, questionRepo, commentCommonRepo, tagCommonRepo)
tagRelRepo := tag.NewTagRelRepo(dataData)
tagRepo := tag.NewTagRepo(dataData, uniqueIDRepo)
revisionRepo := revision.NewRevisionRepo(dataData, uniqueIDRepo)
revisionService := revision_common.NewRevisionService(revisionRepo, userRepo)
tagCommonService := tag_common2.NewTagCommonService(tagCommonRepo, tagRelRepo, tagRepo, revisionService, siteInfoCommonService)
objService := object_info.NewObjService(answerRepo, questionRepo, commentCommonRepo, tagCommonRepo, tagCommonService)
voteRepo := activity_common.NewVoteRepo(dataData, activityRepo)
commentService := comment2.NewCommentService(commentRepo, commentCommonRepo, userCommon, objService, voteRepo)
rankService := rank2.NewRankService(userCommon, userRankRepo, objService, configRepo)
@ -128,12 +135,7 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database,
reportController := controller.NewReportController(reportService, rankService)
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)
voteController := controller.NewVoteController(voteService, rankService)
followRepo := activity_common.NewFollowRepo(dataData, uniqueIDRepo, activityRepo)
tagService := tag2.NewTagService(tagRepo, tagCommonService, revisionService, followRepo, siteInfoCommonService)
tagController := controller.NewTagController(tagService, tagCommonService, rankService)
@ -161,8 +163,8 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database,
searchRepo := search_common.NewSearchRepo(dataData, uniqueIDRepo, userCommon)
searchService := service.NewSearchService(searchParser, searchRepo)
searchController := controller.NewSearchController(searchService)
serviceRevisionService := service.NewRevisionService(revisionRepo, userCommon, questionCommon, answerService)
revisionController := controller.NewRevisionController(serviceRevisionService)
serviceRevisionService := service.NewRevisionService(revisionRepo, userCommon, questionCommon, answerService, objService, questionRepo, answerRepo, tagRepo, tagCommonService)
revisionController := controller.NewRevisionController(serviceRevisionService, rankService)
rankController := controller.NewRankController(rankService)
commonRepo := common.NewCommonRepo(dataData, uniqueIDRepo)
reportHandle := report_handle_backyard.NewReportHandle(questionCommon, commentRepo, configRepo)
@ -180,11 +182,16 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database,
siteinfoController := controller.NewSiteinfoController(siteInfoCommonService)
notificationRepo := notification.NewNotificationRepo(dataData)
notificationCommon := notificationcommon.NewNotificationCommon(dataData, notificationRepo, userCommon, activityRepo, followRepo, objService)
notificationService := notification2.NewNotificationService(dataData, notificationRepo, notificationCommon)
notificationController := controller.NewNotificationController(notificationService)
notificationService := notification2.NewNotificationService(dataData, notificationRepo, notificationCommon, revisionService)
notificationController := controller.NewNotificationController(notificationService, rankService)
dashboardController := controller.NewDashboardController(dashboardService)
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)
activityCommon := activity_common2.NewActivityCommon(activityRepo)
activityActivityRepo := activity.NewActivityRepo(dataData)
commentCommonService := comment_common.NewCommentCommonService(commentCommonRepo)
activityService := activity2.NewActivityService(activityActivityRepo, userCommon, activityCommon, tagCommonService, objService, commentCommonService, revisionService, metaService)
activityController := controller.NewActivityController(activityCommon, activityService)
answerAPIRouter := router.NewAnswerAPIRouter(langController, userController, commentController, reportController, voteController, tagController, followController, collectionController, questionController, answerController, searchController, revisionController, rankController, controller_backyardReportController, userBackyardController, reasonController, themeController, siteInfoController, siteinfoController, notificationController, dashboardController, uploadController, activityController)
swaggerRouter := router.NewSwaggerRouter(swaggerConf)
uiRouter := router.NewUIRouter()
authUserMiddleware := middleware.NewAuthUserMiddleware(authService)

View File

@ -1005,6 +1005,110 @@ const docTemplate = `{
}
}
},
"/answer/api/v1/activity/timeline": {
"get": {
"description": "get object timeline",
"produces": [
"application/json"
],
"tags": [
"Comment"
],
"summary": "get object timeline",
"parameters": [
{
"type": "string",
"description": "object id",
"name": "object_id",
"in": "query"
},
{
"type": "string",
"description": "tag slug name",
"name": "tag_slug_name",
"in": "query"
},
{
"enum": [
"question",
"answer",
"tag"
],
"type": "string",
"description": "object type",
"name": "object_type",
"in": "query"
},
{
"type": "boolean",
"description": "is show vote",
"name": "show_vote",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/handler.RespBody"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/schema.GetObjectTimelineResp"
}
}
}
]
}
}
}
}
},
"/answer/api/v1/activity/timeline/detail": {
"get": {
"description": "get object timeline detail",
"produces": [
"application/json"
],
"tags": [
"Comment"
],
"summary": "get object timeline detail",
"parameters": [
{
"type": "string",
"description": "revision id",
"name": "revision_id",
"in": "query",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/handler.RespBody"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/schema.GetObjectTimelineResp"
}
}
}
]
}
}
}
}
},
"/answer/api/v1/answer": {
"put": {
"security": [
@ -1190,7 +1294,7 @@ const docTemplate = `{
}
}
},
"/answer/api/v1/answer/list": {
"/answer/api/v1/answer/page": {
"get": {
"security": [
{
@ -1210,13 +1314,32 @@ const docTemplate = `{
"summary": "AnswerList",
"parameters": [
{
"description": "AnswerList",
"name": "data",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/schema.AnswerList"
}
"type": "string",
"description": "question_id",
"name": "question_id",
"in": "query",
"required": true
},
{
"type": "string",
"description": "order",
"name": "order",
"in": "query",
"required": true
},
{
"type": "string",
"description": "page",
"name": "page",
"in": "query",
"required": true
},
{
"type": "string",
"description": "page_size",
"name": "page_size",
"in": "query",
"required": true
}
],
"responses": {
@ -2927,6 +3050,141 @@ const docTemplate = `{
}
}
},
"/answer/api/v1/revisions/audit": {
"put": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "revision audit operation:approve or reject",
"produces": [
"application/json"
],
"tags": [
"Revision"
],
"summary": "revision audit",
"parameters": [
{
"description": "audit",
"name": "data",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/schema.RevisionAuditReq"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handler.RespBody"
}
}
}
}
},
"/answer/api/v1/revisions/edit/check": {
"get": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "check can update revision",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Revision"
],
"summary": "check can update revision",
"parameters": [
{
"type": "string",
"default": "string",
"description": "id",
"name": "id",
"in": "query",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handler.RespBody"
}
}
}
}
},
"/answer/api/v1/revisions/unreviewed": {
"get": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "get unreviewed revision list",
"produces": [
"application/json"
],
"tags": [
"Revision"
],
"summary": "get unreviewed revision list",
"parameters": [
{
"type": "string",
"description": "page id",
"name": "page",
"in": "query",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/handler.RespBody"
},
{
"type": "object",
"properties": {
"data": {
"allOf": [
{
"$ref": "#/definitions/pager.PageModel"
},
{
"type": "object",
"properties": {
"list": {
"type": "array",
"items": {
"$ref": "#/definitions/schema.GetUnreviewedRevisionResp"
}
}
}
}
]
}
}
}
]
}
}
}
}
},
"/answer/api/v1/search": {
"get": {
"security": [
@ -3243,10 +3501,7 @@ const docTemplate = `{
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/definitions/schema.GetTagSynonymsResp"
}
"$ref": "#/definitions/schema.GetTagSynonymsResp"
}
}
}
@ -4468,6 +4723,67 @@ const docTemplate = `{
"list": {}
}
},
"schema.ActObjectInfo": {
"type": "object",
"properties": {
"answer_id": {
"type": "string"
},
"display_name": {
"type": "string"
},
"object_type": {
"type": "string"
},
"question_id": {
"type": "string"
},
"title": {
"type": "string"
},
"username": {
"type": "string"
}
}
},
"schema.ActObjectTimeline": {
"type": "object",
"properties": {
"activity_id": {
"type": "string"
},
"activity_type": {
"type": "string"
},
"cancelled": {
"type": "boolean"
},
"cancelled_at": {
"type": "integer"
},
"comment": {
"type": "string"
},
"created_at": {
"type": "integer"
},
"object_id": {
"type": "string"
},
"object_type": {
"type": "string"
},
"revision_id": {
"type": "string"
},
"user_display_name": {
"type": "string"
},
"username": {
"type": "string"
}
}
},
"schema.ActionRecordResp": {
"type": "object",
"properties": {
@ -4578,27 +4894,6 @@ const docTemplate = `{
}
}
},
"schema.AnswerList": {
"type": "object",
"properties": {
"order": {
"description": "1 Default 2 time",
"type": "string"
},
"page": {
"description": "Query number of pages",
"type": "integer"
},
"page_size": {
"description": "Search page size",
"type": "integer"
},
"question_id": {
"description": "question_id",
"type": "string"
}
}
},
"schema.AnswerUpdateReq": {
"type": "object",
"properties": {
@ -4874,6 +5169,20 @@ const docTemplate = `{
}
}
},
"schema.GetObjectTimelineResp": {
"type": "object",
"properties": {
"object_info": {
"$ref": "#/definitions/schema.ActObjectInfo"
},
"timeline": {
"type": "array",
"items": {
"$ref": "#/definitions/schema.ActObjectTimeline"
}
}
}
},
"schema.GetOtherUserInfoByUsernameResp": {
"type": "object",
"properties": {
@ -5248,21 +5557,33 @@ const docTemplate = `{
"schema.GetTagSynonymsResp": {
"type": "object",
"properties": {
"display_name": {
"description": "display name",
"member_actions": {
"description": "MemberActions",
"type": "array",
"items": {
"$ref": "#/definitions/schema.PermissionMemberAction"
}
},
"synonyms": {
"description": "synonyms",
"type": "array",
"items": {
"$ref": "#/definitions/schema.TagSynonym"
}
}
}
},
"schema.GetUnreviewedRevisionResp": {
"type": "object",
"properties": {
"info": {
"$ref": "#/definitions/schema.UnreviewedRevisionInfoInfo"
},
"type": {
"type": "string"
},
"main_tag_slug_name": {
"description": "if main tag slug name is not empty, this tag is synonymous with the main tag",
"type": "string"
},
"slug_name": {
"description": "slug name",
"type": "string"
},
"tag_id": {
"description": "tag id",
"type": "string"
"unreviewed_info": {
"$ref": "#/definitions/schema.GetRevisionResp"
}
}
},
@ -5758,6 +6079,23 @@ const docTemplate = `{
}
}
},
"schema.RevisionAuditReq": {
"type": "object",
"required": [
"id",
"operation"
],
"properties": {
"id": {
"description": "object id",
"type": "string"
},
"operation": {
"description": "approve or reject",
"type": "string"
}
}
},
"schema.SearchListResp": {
"type": "object",
"properties": {
@ -5887,9 +6225,7 @@ const docTemplate = `{
"type": "object",
"required": [
"contact_email",
"description",
"name",
"short_description",
"site_url"
],
"properties": {
@ -5919,9 +6255,7 @@ const docTemplate = `{
"type": "object",
"required": [
"contact_email",
"description",
"name",
"short_description",
"site_url"
],
"properties": {
@ -6109,6 +6443,50 @@ const docTemplate = `{
}
}
},
"schema.TagSynonym": {
"type": "object",
"properties": {
"display_name": {
"description": "display name",
"type": "string"
},
"main_tag_slug_name": {
"description": "if main tag slug name is not empty, this tag is synonymous with the main tag",
"type": "string"
},
"slug_name": {
"description": "slug name",
"type": "string"
},
"tag_id": {
"description": "tag id",
"type": "string"
}
}
},
"schema.UnreviewedRevisionInfoInfo": {
"type": "object",
"properties": {
"content": {
"type": "string"
},
"html": {
"type": "string"
},
"object_id": {
"type": "string"
},
"tags": {
"type": "array",
"items": {
"$ref": "#/definitions/schema.TagResp"
}
},
"title": {
"type": "string"
}
}
},
"schema.UpdateCommentReq": {
"type": "object",
"required": [

View File

@ -993,6 +993,110 @@
}
}
},
"/answer/api/v1/activity/timeline": {
"get": {
"description": "get object timeline",
"produces": [
"application/json"
],
"tags": [
"Comment"
],
"summary": "get object timeline",
"parameters": [
{
"type": "string",
"description": "object id",
"name": "object_id",
"in": "query"
},
{
"type": "string",
"description": "tag slug name",
"name": "tag_slug_name",
"in": "query"
},
{
"enum": [
"question",
"answer",
"tag"
],
"type": "string",
"description": "object type",
"name": "object_type",
"in": "query"
},
{
"type": "boolean",
"description": "is show vote",
"name": "show_vote",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/handler.RespBody"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/schema.GetObjectTimelineResp"
}
}
}
]
}
}
}
}
},
"/answer/api/v1/activity/timeline/detail": {
"get": {
"description": "get object timeline detail",
"produces": [
"application/json"
],
"tags": [
"Comment"
],
"summary": "get object timeline detail",
"parameters": [
{
"type": "string",
"description": "revision id",
"name": "revision_id",
"in": "query",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/handler.RespBody"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/schema.GetObjectTimelineResp"
}
}
}
]
}
}
}
}
},
"/answer/api/v1/answer": {
"put": {
"security": [
@ -1178,7 +1282,7 @@
}
}
},
"/answer/api/v1/answer/list": {
"/answer/api/v1/answer/page": {
"get": {
"security": [
{
@ -1198,13 +1302,32 @@
"summary": "AnswerList",
"parameters": [
{
"description": "AnswerList",
"name": "data",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/schema.AnswerList"
}
"type": "string",
"description": "question_id",
"name": "question_id",
"in": "query",
"required": true
},
{
"type": "string",
"description": "order",
"name": "order",
"in": "query",
"required": true
},
{
"type": "string",
"description": "page",
"name": "page",
"in": "query",
"required": true
},
{
"type": "string",
"description": "page_size",
"name": "page_size",
"in": "query",
"required": true
}
],
"responses": {
@ -2915,6 +3038,141 @@
}
}
},
"/answer/api/v1/revisions/audit": {
"put": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "revision audit operation:approve or reject",
"produces": [
"application/json"
],
"tags": [
"Revision"
],
"summary": "revision audit",
"parameters": [
{
"description": "audit",
"name": "data",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/schema.RevisionAuditReq"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handler.RespBody"
}
}
}
}
},
"/answer/api/v1/revisions/edit/check": {
"get": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "check can update revision",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Revision"
],
"summary": "check can update revision",
"parameters": [
{
"type": "string",
"default": "string",
"description": "id",
"name": "id",
"in": "query",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handler.RespBody"
}
}
}
}
},
"/answer/api/v1/revisions/unreviewed": {
"get": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "get unreviewed revision list",
"produces": [
"application/json"
],
"tags": [
"Revision"
],
"summary": "get unreviewed revision list",
"parameters": [
{
"type": "string",
"description": "page id",
"name": "page",
"in": "query",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/handler.RespBody"
},
{
"type": "object",
"properties": {
"data": {
"allOf": [
{
"$ref": "#/definitions/pager.PageModel"
},
{
"type": "object",
"properties": {
"list": {
"type": "array",
"items": {
"$ref": "#/definitions/schema.GetUnreviewedRevisionResp"
}
}
}
}
]
}
}
}
]
}
}
}
}
},
"/answer/api/v1/search": {
"get": {
"security": [
@ -3231,10 +3489,7 @@
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/definitions/schema.GetTagSynonymsResp"
}
"$ref": "#/definitions/schema.GetTagSynonymsResp"
}
}
}
@ -4456,6 +4711,67 @@
"list": {}
}
},
"schema.ActObjectInfo": {
"type": "object",
"properties": {
"answer_id": {
"type": "string"
},
"display_name": {
"type": "string"
},
"object_type": {
"type": "string"
},
"question_id": {
"type": "string"
},
"title": {
"type": "string"
},
"username": {
"type": "string"
}
}
},
"schema.ActObjectTimeline": {
"type": "object",
"properties": {
"activity_id": {
"type": "string"
},
"activity_type": {
"type": "string"
},
"cancelled": {
"type": "boolean"
},
"cancelled_at": {
"type": "integer"
},
"comment": {
"type": "string"
},
"created_at": {
"type": "integer"
},
"object_id": {
"type": "string"
},
"object_type": {
"type": "string"
},
"revision_id": {
"type": "string"
},
"user_display_name": {
"type": "string"
},
"username": {
"type": "string"
}
}
},
"schema.ActionRecordResp": {
"type": "object",
"properties": {
@ -4566,27 +4882,6 @@
}
}
},
"schema.AnswerList": {
"type": "object",
"properties": {
"order": {
"description": "1 Default 2 time",
"type": "string"
},
"page": {
"description": "Query number of pages",
"type": "integer"
},
"page_size": {
"description": "Search page size",
"type": "integer"
},
"question_id": {
"description": "question_id",
"type": "string"
}
}
},
"schema.AnswerUpdateReq": {
"type": "object",
"properties": {
@ -4862,6 +5157,20 @@
}
}
},
"schema.GetObjectTimelineResp": {
"type": "object",
"properties": {
"object_info": {
"$ref": "#/definitions/schema.ActObjectInfo"
},
"timeline": {
"type": "array",
"items": {
"$ref": "#/definitions/schema.ActObjectTimeline"
}
}
}
},
"schema.GetOtherUserInfoByUsernameResp": {
"type": "object",
"properties": {
@ -5236,21 +5545,33 @@
"schema.GetTagSynonymsResp": {
"type": "object",
"properties": {
"display_name": {
"description": "display name",
"member_actions": {
"description": "MemberActions",
"type": "array",
"items": {
"$ref": "#/definitions/schema.PermissionMemberAction"
}
},
"synonyms": {
"description": "synonyms",
"type": "array",
"items": {
"$ref": "#/definitions/schema.TagSynonym"
}
}
}
},
"schema.GetUnreviewedRevisionResp": {
"type": "object",
"properties": {
"info": {
"$ref": "#/definitions/schema.UnreviewedRevisionInfoInfo"
},
"type": {
"type": "string"
},
"main_tag_slug_name": {
"description": "if main tag slug name is not empty, this tag is synonymous with the main tag",
"type": "string"
},
"slug_name": {
"description": "slug name",
"type": "string"
},
"tag_id": {
"description": "tag id",
"type": "string"
"unreviewed_info": {
"$ref": "#/definitions/schema.GetRevisionResp"
}
}
},
@ -5746,6 +6067,23 @@
}
}
},
"schema.RevisionAuditReq": {
"type": "object",
"required": [
"id",
"operation"
],
"properties": {
"id": {
"description": "object id",
"type": "string"
},
"operation": {
"description": "approve or reject",
"type": "string"
}
}
},
"schema.SearchListResp": {
"type": "object",
"properties": {
@ -5875,9 +6213,7 @@
"type": "object",
"required": [
"contact_email",
"description",
"name",
"short_description",
"site_url"
],
"properties": {
@ -5907,9 +6243,7 @@
"type": "object",
"required": [
"contact_email",
"description",
"name",
"short_description",
"site_url"
],
"properties": {
@ -6097,6 +6431,50 @@
}
}
},
"schema.TagSynonym": {
"type": "object",
"properties": {
"display_name": {
"description": "display name",
"type": "string"
},
"main_tag_slug_name": {
"description": "if main tag slug name is not empty, this tag is synonymous with the main tag",
"type": "string"
},
"slug_name": {
"description": "slug name",
"type": "string"
},
"tag_id": {
"description": "tag id",
"type": "string"
}
}
},
"schema.UnreviewedRevisionInfoInfo": {
"type": "object",
"properties": {
"content": {
"type": "string"
},
"html": {
"type": "string"
},
"object_id": {
"type": "string"
},
"tags": {
"type": "array",
"items": {
"$ref": "#/definitions/schema.TagResp"
}
},
"title": {
"type": "string"
}
}
},
"schema.UpdateCommentReq": {
"type": "object",
"required": [

View File

@ -89,6 +89,46 @@ definitions:
type: integer
list: {}
type: object
schema.ActObjectInfo:
properties:
answer_id:
type: string
display_name:
type: string
object_type:
type: string
question_id:
type: string
title:
type: string
username:
type: string
type: object
schema.ActObjectTimeline:
properties:
activity_id:
type: string
activity_type:
type: string
cancelled:
type: boolean
cancelled_at:
type: integer
comment:
type: string
created_at:
type: integer
object_id:
type: string
object_type:
type: string
revision_id:
type: string
user_display_name:
type: string
username:
type: string
type: object
schema.ActionRecordResp:
properties:
captcha_id:
@ -166,21 +206,6 @@ definitions:
description: question_id
type: string
type: object
schema.AnswerList:
properties:
order:
description: 1 Default 2 time
type: string
page:
description: Query number of pages
type: integer
page_size:
description: Search page size
type: integer
question_id:
description: question_id
type: string
type: object
schema.AnswerUpdateReq:
properties:
content:
@ -379,6 +404,15 @@ definitions:
description: tag id
type: string
type: object
schema.GetObjectTimelineResp:
properties:
object_info:
$ref: '#/definitions/schema.ActObjectInfo'
timeline:
items:
$ref: '#/definitions/schema.ActObjectTimeline'
type: array
type: object
schema.GetOtherUserInfoByUsernameResp:
properties:
answer_count:
@ -650,19 +684,25 @@ definitions:
type: object
schema.GetTagSynonymsResp:
properties:
display_name:
description: display name
type: string
main_tag_slug_name:
description: if main tag slug name is not empty, this tag is synonymous with
the main tag
type: string
slug_name:
description: slug name
type: string
tag_id:
description: tag id
member_actions:
description: MemberActions
items:
$ref: '#/definitions/schema.PermissionMemberAction'
type: array
synonyms:
description: synonyms
items:
$ref: '#/definitions/schema.TagSynonym'
type: array
type: object
schema.GetUnreviewedRevisionResp:
properties:
info:
$ref: '#/definitions/schema.UnreviewedRevisionInfoInfo'
type:
type: string
unreviewed_info:
$ref: '#/definitions/schema.GetRevisionResp'
type: object
schema.GetUserPageResp:
properties:
@ -1024,6 +1064,18 @@ definitions:
- flagged_type
- id
type: object
schema.RevisionAuditReq:
properties:
id:
description: object id
type: string
operation:
description: approve or reject
type: string
required:
- id
- operation
type: object
schema.SearchListResp:
properties:
count:
@ -1130,9 +1182,7 @@ definitions:
type: string
required:
- contact_email
- description
- name
- short_description
- site_url
type: object
schema.SiteGeneralResp:
@ -1154,9 +1204,7 @@ definitions:
type: string
required:
- contact_email
- description
- name
- short_description
- site_url
type: object
schema.SiteInterfaceReq:
@ -1271,6 +1319,37 @@ definitions:
slug_name:
type: string
type: object
schema.TagSynonym:
properties:
display_name:
description: display name
type: string
main_tag_slug_name:
description: if main tag slug name is not empty, this tag is synonymous with
the main tag
type: string
slug_name:
description: slug name
type: string
tag_id:
description: tag id
type: string
type: object
schema.UnreviewedRevisionInfoInfo:
properties:
content:
type: string
html:
type: string
object_id:
type: string
tags:
items:
$ref: '#/definitions/schema.TagResp'
type: array
title:
type: string
type: object
schema.UpdateCommentReq:
properties:
comment_id:
@ -2189,6 +2268,69 @@ paths:
summary: get user page
tags:
- admin
/answer/api/v1/activity/timeline:
get:
description: get object timeline
parameters:
- description: object id
in: query
name: object_id
type: string
- description: tag slug name
in: query
name: tag_slug_name
type: string
- description: object type
enum:
- question
- answer
- tag
in: query
name: object_type
type: string
- description: is show vote
in: query
name: show_vote
type: boolean
produces:
- application/json
responses:
"200":
description: OK
schema:
allOf:
- $ref: '#/definitions/handler.RespBody'
- properties:
data:
$ref: '#/definitions/schema.GetObjectTimelineResp'
type: object
summary: get object timeline
tags:
- Comment
/answer/api/v1/activity/timeline/detail:
get:
description: get object timeline detail
parameters:
- description: revision id
in: query
name: revision_id
required: true
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
allOf:
- $ref: '#/definitions/handler.RespBody'
- properties:
data:
$ref: '#/definitions/schema.GetObjectTimelineResp'
type: object
summary: get object timeline detail
tags:
- Comment
/answer/api/v1/answer:
delete:
consumes:
@ -2305,18 +2447,32 @@ paths:
summary: Get Answer
tags:
- api-answer
/answer/api/v1/answer/list:
/answer/api/v1/answer/page:
get:
consumes:
- application/json
description: AnswerList <br> <b>order</b> (default or updated)
parameters:
- description: AnswerList
in: body
name: data
- description: question_id
in: query
name: question_id
required: true
schema:
$ref: '#/definitions/schema.AnswerList'
type: string
- description: order
in: query
name: order
required: true
type: string
- description: page
in: query
name: page
required: true
type: string
- description: page_size
in: query
name: page_size
required: true
type: string
produces:
- application/json
responses:
@ -3362,6 +3518,85 @@ paths:
summary: get revision list
tags:
- Revision
/answer/api/v1/revisions/audit:
put:
description: revision audit operation:approve or reject
parameters:
- description: audit
in: body
name: data
required: true
schema:
$ref: '#/definitions/schema.RevisionAuditReq'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handler.RespBody'
security:
- ApiKeyAuth: []
summary: revision audit
tags:
- Revision
/answer/api/v1/revisions/edit/check:
get:
consumes:
- application/json
description: check can update revision
parameters:
- default: string
description: id
in: query
name: id
required: true
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handler.RespBody'
security:
- ApiKeyAuth: []
summary: check can update revision
tags:
- Revision
/answer/api/v1/revisions/unreviewed:
get:
description: get unreviewed revision list
parameters:
- description: page id
in: query
name: page
required: true
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
allOf:
- $ref: '#/definitions/handler.RespBody'
- properties:
data:
allOf:
- $ref: '#/definitions/pager.PageModel'
- properties:
list:
items:
$ref: '#/definitions/schema.GetUnreviewedRevisionResp'
type: array
type: object
type: object
security:
- ApiKeyAuth: []
summary: get unreviewed revision list
tags:
- Revision
/answer/api/v1/search:
get:
description: search object
@ -3557,9 +3792,7 @@ paths:
- $ref: '#/definitions/handler.RespBody'
- properties:
data:
items:
$ref: '#/definitions/schema.GetTagSynonymsResp'
type: array
$ref: '#/definitions/schema.GetTagSynonymsResp'
type: object
summary: get tag synonyms
tags:

2
go.mod
View File

@ -65,6 +65,7 @@ require (
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/google/subcommands v1.0.1 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/imdario/mergo v0.3.12 // indirect
github.com/inconshreveable/mousetrap v1.0.1 // indirect
@ -105,6 +106,7 @@ require (
go.uber.org/multierr v1.8.0 // indirect
go.uber.org/zap v1.23.0 // indirect
golang.org/x/image v0.1.0 // indirect
golang.org/x/mod v0.6.0 // indirect
golang.org/x/sys v0.1.0 // indirect
golang.org/x/text v0.4.0 // indirect
golang.org/x/tools v0.2.0 // indirect

2
go.sum
View File

@ -284,6 +284,7 @@ github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLe
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
github.com/google/subcommands v1.0.1 h1:/eqq+otEXm5vhfBrbREPCSVQbvofip6kIz+mX5TUH7k=
github.com/google/subcommands v1.0.1/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
@ -779,6 +780,7 @@ golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.6.0 h1:b9gGHsz9/HhJ3HF5DHQytPpuwocVTChQJK3AvoLRD5I=
golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=

View File

@ -1,4 +1,5 @@
# The following fields are used for back-end
backend:
base:
success:
@ -26,6 +27,10 @@ backend:
answer:
not_found:
other: "Answer do not found."
cannot_deleted:
other: "No permission to delete."
cannot_update:
other: "No permission to update."
comment:
edit_without_permission:
other: "Comment are not allowed to edit."
@ -63,6 +68,12 @@ backend:
question:
not_found:
other: "Question not found."
cannot_deleted:
other: "No permission to delete."
cannot_close:
other: "No permission to close."
cannot_update:
other: "No permission to update."
rank:
fail_to_meet_the_condition:
other: "Rank fail to meet the condition."
@ -76,11 +87,20 @@ backend:
other: "Tag not found."
recommend_tag_not_found:
other: "Recommend Tag is not exist."
recommend_tag_enter:
other: "Please enter at least one required tag."
not_contain_synonym_tags:
other: "Should not contain synonym tags."
cannot_update:
other: "No permission to update."
theme:
not_found:
other: "Theme not found."
revision:
review_underway:
other: "Can't edit currently, there is a version in the review queue."
no_permission:
other: "No permission to Revision."
user:
email_or_password_wrong:
other: *email_or_password_wrong
@ -94,18 +114,17 @@ backend:
other: "Username is already in use."
set_avatar:
other: "Avatar set failed."
config:
read_config_failed:
other: "Read config failed"
database:
connection_failed:
other: "Database connection failed"
create_table_failed:
other: "Create table failed"
install:
create_config_failed:
other: "Cant create the config.yaml file."
config:
read_config_failed:
other: "Read config failed"
database:
connection_failed:
other: "Database connection failed"
create_table_failed:
other: "Create table failed"
install:
create_config_failed:
other: "Cant create the config.yaml file."
report:
spam:
name:
@ -683,6 +702,7 @@ ui:
update_password: Password changed successfully.
flag_success: Thanks for flagging.
fobidden_operate_self: Forbidden to operate on yourself
review: Your revision will show after review.
related_question:
title: Related Questions
btn: Add question

View File

@ -73,6 +73,14 @@ backend:
tag:
not_found:
other: "标签未找到"
recommend_tag_not_found:
other: "推荐标签不存在"
recommend_tag_enter:
other: "请输入至少一个必需的标签。"
not_contain_synonym_tags:
other: "不应包含同义词标签。"
cannot_update:
other: "没有更新标签权限。"
theme:
not_found:
other: "主题未找到"
@ -89,6 +97,11 @@ backend:
other: "用户名已被使用"
set_avatar:
other: "头像设置错误"
revision:
review_underway:
other: "目前无法编辑,有一个版本在审阅队列中。"
no_permission:
other: "无权限修改"
report:
spam:

View File

@ -0,0 +1,52 @@
package constant
type ActivityTypeKey string
const (
ActEdited = "edited"
ActClosed = "closed"
ActVotedDown = "voted_down"
ActVotedUp = "voted_up"
ActVoteDown = "vote_down"
ActVoteUp = "vote_up"
ActUpVote = "upvote"
ActDownVote = "downvote"
ActFollow = "follow"
ActAccepted = "accepted"
ActAccept = "accept"
)
const (
ActQuestionAsked ActivityTypeKey = "question.asked"
ActQuestionClosed ActivityTypeKey = "question.closed"
ActQuestionReopened ActivityTypeKey = "question.reopened"
ActQuestionAnswered ActivityTypeKey = "question.answered"
ActQuestionCommented ActivityTypeKey = "question.commented"
ActQuestionAccept ActivityTypeKey = "question.accept"
ActQuestionUpvote ActivityTypeKey = "question.upvote"
ActQuestionDownVote ActivityTypeKey = "question.downvote"
ActQuestionEdited ActivityTypeKey = "question.edited"
ActQuestionRollback ActivityTypeKey = "question.rollback"
ActQuestionDeleted ActivityTypeKey = "question.deleted"
ActQuestionUndeleted ActivityTypeKey = "question.undeleted"
)
const (
ActAnswerAnswered ActivityTypeKey = "answer.answered"
ActAnswerCommented ActivityTypeKey = "answer.commented"
ActAnswerAccept ActivityTypeKey = "answer.accept"
ActAnswerUpvote ActivityTypeKey = "answer.upvote"
ActAnswerDownVote ActivityTypeKey = "answer.downvote"
ActAnswerEdited ActivityTypeKey = "answer.edited"
ActAnswerRollback ActivityTypeKey = "answer.rollback"
ActAnswerDeleted ActivityTypeKey = "answer.deleted"
ActAnswerUndeleted ActivityTypeKey = "answer.undeleted"
)
const (
ActTagCreated ActivityTypeKey = "tag.created"
ActTagEdited ActivityTypeKey = "tag.edited"
ActTagRollback ActivityTypeKey = "tag.rollback"
ActTagDeleted ActivityTypeKey = "tag.deleted"
ActTagUndeleted ActivityTypeKey = "tag.undeleted"
)

View File

@ -1,6 +1,8 @@
package handler
import (
"context"
"github.com/answerdev/answer/internal/base/constant"
"github.com/gin-gonic/gin"
"github.com/segmentfault/pacman/i18n"
@ -18,3 +20,12 @@ func GetLang(ctx *gin.Context) i18n.Language {
return i18n.DefaultLang
}
}
// GetLangByCtx get language from header
func GetLangByCtx(ctx context.Context) i18n.Language {
acceptLanguage, ok := ctx.Value(constant.AcceptLanguageFlag).(i18n.Language)
if ok {
return acceptLanguage
}
return i18n.DefaultLang
}

View File

@ -113,15 +113,20 @@ func (am *AuthUserMiddleware) CmsAuth() gin.HandlerFunc {
// GetLoginUserIDFromContext get user id from context
func GetLoginUserIDFromContext(ctx *gin.Context) (userID string) {
userInfo, exist := ctx.Get(ctxUUIDKey)
if !exist {
userInfo := GetUserInfoFromContext(ctx)
if userInfo == nil {
return ""
}
u, ok := userInfo.(*entity.UserCacheInfo)
if !ok {
return ""
return userInfo.UserID
}
// GetIsAdminFromContext get user is admin from context
func GetIsAdminFromContext(ctx *gin.Context) (isAdmin bool) {
userInfo := GetUserInfoFromContext(ctx)
if userInfo == nil {
return false
}
return u.UserID
return userInfo.IsAdmin
}
// GetUserInfoFromContext get user info from context

View File

@ -14,38 +14,47 @@ const (
)
const (
EmailOrPasswordWrong = "error.object.email_or_password_incorrect"
CommentNotFound = "error.comment.not_found"
QuestionNotFound = "error.question.not_found"
AnswerNotFound = "error.answer.not_found"
CommentEditWithoutPermission = "error.comment.edit_without_permission"
DisallowVote = "error.object.disallow_vote"
DisallowFollow = "error.object.disallow_follow"
DisallowVoteYourSelf = "error.object.disallow_vote_your_self"
CaptchaVerificationFailed = "error.object.captcha_verification_failed"
EmailOrPasswordWrong = "error.object.email_or_password_incorrect"
CommentNotFound = "error.comment.not_found"
QuestionNotFound = "error.question.not_found"
QuestionCannotDeleted = "error.question.cannot_deleted"
QuestionCannotClose = "error.question.cannot_close"
QuestionCannotUpdate = "error.question.cannot_update"
AnswerNotFound = "error.answer.not_found"
AnswerCannotDeleted = "error.answer.cannot_deleted"
AnswerCannotUpdate = "error.answer.cannot_update"
CommentEditWithoutPermission = "error.comment.edit_without_permission"
DisallowVote = "error.object.disallow_vote"
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"
UserSetAvatar = "error.user.set_avatar"
EmailDuplicate = "error.email.duplicate"
EmailVerifyURLExpired = "error.email.verify_url_expired"
EmailNeedToBeVerified = "error.email.need_to_be_verified"
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"
ReportHandleFailed = "error.report.handle_failed"
ReportNotFound = "error.report.not_found"
ReadConfigFailed = "error.config.read_config_failed"
DatabaseConnectionFailed = "error.database.connection_failed"
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"
UserNotFound = "error.user.not_found"
UsernameInvalid = "error.user.username_invalid"
UsernameDuplicate = "error.user.username_duplicate"
UserSetAvatar = "error.user.set_avatar"
EmailDuplicate = "error.email.duplicate"
EmailVerifyURLExpired = "error.email.verify_url_expired"
EmailNeedToBeVerified = "error.email.need_to_be_verified"
UserSuspended = "error.user.suspended"
ObjectNotFound = "error.object.not_found"
TagNotFound = "error.tag.not_found"
TagNotContainSynonym = "error.tag.not_contain_synonym_tags"
TagCannotUpdate = "error.tag.cannot_update"
RankFailToMeetTheCondition = "error.rank.fail_to_meet_the_condition"
ThemeNotFound = "error.theme.not_found"
LangNotFound = "error.lang.not_found"
ReportHandleFailed = "error.report.handle_failed"
ReportNotFound = "error.report.not_found"
ReadConfigFailed = "error.config.read_config_failed"
DatabaseConnectionFailed = "error.database.connection_failed"
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"
RecommendTagEnter = "error.tag.recommend_tag_enter"
RevisionReviewUnderway = "error.revision.review_underway"
RevisionNoPermission = "error.revision.no_permission"
)

View File

@ -2,6 +2,7 @@ package cli
import (
"fmt"
"os"
"path/filepath"
"github.com/answerdev/answer/configs"
@ -96,6 +97,12 @@ func installI18nBundle() {
if err != nil {
continue
}
if dir.CheckFileExist(path) {
fmt.Printf("[i18n] install %s file exist, try to replace it\n", item.Name())
if err = os.Remove(path); err != nil {
fmt.Println(err)
}
}
fmt.Printf("[i18n] install %s bundle...\n", item.Name())
err = writer.WriteFile(path, string(content))
if err != nil {

View File

@ -0,0 +1,68 @@
package controller
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/activity"
"github.com/answerdev/answer/internal/service/activity_common"
"github.com/gin-gonic/gin"
)
type ActivityController struct {
activityCommonService *activity_common.ActivityCommon
activityService *activity.ActivityService
}
// NewActivityController new activity controller.
func NewActivityController(
activityCommonService *activity_common.ActivityCommon,
activityService *activity.ActivityService) *ActivityController {
return &ActivityController{activityCommonService: activityCommonService, activityService: activityService}
}
// GetObjectTimeline get object timeline
// @Summary get object timeline
// @Description get object timeline
// @Tags Comment
// @Produce json
// @Param object_id query string false "object id"
// @Param tag_slug_name query string false "tag slug name"
// @Param object_type query string false "object type" Enums(question, answer, tag)
// @Param show_vote query boolean false "is show vote"
// @Success 200 {object} handler.RespBody{data=schema.GetObjectTimelineResp}
// @Router /answer/api/v1/activity/timeline [get]
func (ac *ActivityController) GetObjectTimeline(ctx *gin.Context) {
req := &schema.GetObjectTimelineReq{}
if handler.BindAndCheck(ctx, req) {
return
}
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
if userInfo := middleware.GetUserInfoFromContext(ctx); userInfo != nil {
req.IsAdmin = userInfo.IsAdmin
}
resp, err := ac.activityService.GetObjectTimeline(ctx, req)
handler.HandleResponse(ctx, err, resp)
}
// GetObjectTimelineDetail get object timeline detail
// @Summary get object timeline detail
// @Description get object timeline detail
// @Tags Comment
// @Produce json
// @Param revision_id query string true "revision id"
// @Success 200 {object} handler.RespBody{data=schema.GetObjectTimelineResp}
// @Router /answer/api/v1/activity/timeline/detail [get]
func (ac *ActivityController) GetObjectTimelineDetail(ctx *gin.Context) {
req := &schema.GetObjectTimelineDetailReq{}
if handler.BindAndCheck(ctx, req) {
return
}
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
resp, err := ac.activityService.GetObjectTimelineDetail(ctx, req)
handler.HandleResponse(ctx, err, resp)
}

View File

@ -6,7 +6,6 @@ import (
"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/entity"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service"
"github.com/answerdev/answer/internal/service/dashboard"
@ -51,12 +50,18 @@ func (ac *AnswerController) RemoveAnswer(ctx *gin.Context) {
}
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
if can, err := ac.rankService.CheckRankPermission(ctx, req.UserID, rank.AnswerDeleteRank); err != nil || !can {
handler.HandleResponse(ctx, err, errors.Forbidden(reason.RankFailToMeetTheCondition))
req.IsAdmin = middleware.GetIsAdminFromContext(ctx)
can, err := ac.rankService.CheckOperationPermission(ctx, req.UserID, rank.AnswerDeleteRank, req.ID)
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
}
if !can {
handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil)
return
}
err := ac.answerService.RemoveAnswer(ctx, req)
err = ac.answerService.RemoveAnswer(ctx, req)
handler.HandleResponse(ctx, err, nil)
}
@ -105,8 +110,13 @@ func (ac *AnswerController) Add(ctx *gin.Context) {
}
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
if can, err := ac.rankService.CheckRankPermission(ctx, req.UserID, rank.AnswerAddRank); err != nil || !can {
handler.HandleResponse(ctx, err, errors.Forbidden(reason.RankFailToMeetTheCondition))
can, err := ac.rankService.CheckOperationPermission(ctx, req.UserID, rank.AnswerAddRank, "")
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
}
if !can {
handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil)
return
}
@ -148,30 +158,32 @@ func (ac *AnswerController) Update(ctx *gin.Context) {
}
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
if can, err := ac.rankService.CheckRankPermission(ctx, req.UserID, rank.AnswerEditRank); err != nil || !can {
handler.HandleResponse(ctx, err, errors.Forbidden(reason.RankFailToMeetTheCondition))
canList, err := ac.rankService.CheckOperationPermissions(ctx, req.UserID, []string{
rank.AnswerEditRank,
rank.AnswerEditWithoutReviewRank,
}, req.ID)
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
}
req.CanEdit = canList[0]
req.NoNeedReview = canList[1]
if !req.CanEdit {
handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil)
return
}
_, err := ac.answerService.Update(ctx, req)
_, err = ac.answerService.Update(ctx, req)
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
}
info, questionInfo, has, err := ac.answerService.Get(ctx, req.ID, req.UserID)
_, _, _, err = ac.answerService.Get(ctx, req.ID, req.UserID)
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
}
if !has {
// todo !has
handler.HandleResponse(ctx, nil, nil)
return
}
handler.HandleResponse(ctx, nil, gin.H{
"info": info,
"question": questionInfo,
})
handler.HandleResponse(ctx, nil, &schema.AnswerUpdateResp{WaitForReview: !req.NoNeedReview})
}
// AnswerList godoc
@ -181,15 +193,31 @@ func (ac *AnswerController) Update(ctx *gin.Context) {
// @Security ApiKeyAuth
// @Accept json
// @Produce json
// @Param data body schema.AnswerList true "AnswerList"
// @Param question_id query string true "question_id"
// @Param order query string true "order"
// @Param page query string true "page"
// @Param page_size query string true "page_size"
// @Success 200 {string} string ""
// @Router /answer/api/v1/answer/list [get]
// @Router /answer/api/v1/answer/page [get]
func (ac *AnswerController) AnswerList(ctx *gin.Context) {
req := &schema.AnswerList{}
req := &schema.AnswerListReq{}
if handler.BindAndCheck(ctx, req) {
return
}
req.LoginUserID = middleware.GetLoginUserIDFromContext(ctx)
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
canList, err := ac.rankService.CheckOperationPermissions(ctx, req.UserID, []string{
rank.AnswerEditRank,
rank.AnswerDeleteRank,
}, "")
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
}
req.CanEdit = canList[0]
req.CanDelete = canList[1]
list, count, err := ac.answerService.SearchList(ctx, req)
if err != nil {
handler.HandleResponse(ctx, err, nil)
@ -218,12 +246,17 @@ func (ac *AnswerController) Adopted(ctx *gin.Context) {
}
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
if can, err := ac.rankService.CheckRankPermission(ctx, req.UserID, rank.AnswerAcceptRank); err != nil || !can {
handler.HandleResponse(ctx, err, errors.Forbidden(reason.RankFailToMeetTheCondition))
can, err := ac.rankService.CheckOperationPermission(ctx, req.UserID, rank.AnswerAcceptRank, req.QuestionID)
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
}
if !can {
handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil)
return
}
err := ac.answerService.UpdateAdopted(ctx, req)
err = ac.answerService.UpdateAdopted(ctx, req)
handler.HandleResponse(ctx, err, nil)
}
@ -238,10 +271,13 @@ func (ac *AnswerController) Adopted(ctx *gin.Context) {
// @Router /answer/admin/api/answer/status [put]
// @Success 200 {object} handler.RespBody
func (ac *AnswerController) AdminSetAnswerStatus(ctx *gin.Context) {
req := &entity.AdminSetAnswerStatusRequest{}
req := &schema.AdminSetAnswerStatusRequest{}
if handler.BindAndCheck(ctx, req) {
return
}
err := ac.answerService.AdminSetAnswerStatus(ctx, req.AnswerID, req.StatusStr)
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
err := ac.answerService.AdminSetAnswerStatus(ctx, req)
handler.HandleResponse(ctx, err, gin.H{})
}

View File

@ -41,8 +41,20 @@ func (cc *CommentController) AddComment(ctx *gin.Context) {
}
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
if can, err := cc.rankService.CheckRankPermission(ctx, req.UserID, rank.CommentAddRank); err != nil || !can {
handler.HandleResponse(ctx, err, errors.Forbidden(reason.RankFailToMeetTheCondition))
canList, err := cc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{
rank.CommentAddRank,
rank.CommentEditRank,
rank.CommentDeleteRank,
}, "")
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
}
req.CanAdd = canList[0]
req.CanEdit = canList[1]
req.CanDelete = canList[2]
if !req.CanAdd {
handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil)
return
}
@ -67,12 +79,17 @@ func (cc *CommentController) RemoveComment(ctx *gin.Context) {
}
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
if can, err := cc.rankService.CheckRankPermission(ctx, req.UserID, rank.CommentDeleteRank); err != nil || !can {
handler.HandleResponse(ctx, err, errors.Forbidden(reason.RankFailToMeetTheCondition))
can, err := cc.rankService.CheckOperationPermission(ctx, req.UserID, rank.CommentDeleteRank, req.CommentID)
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
}
if !can {
handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil)
return
}
err := cc.commentService.RemoveComment(ctx, req)
err = cc.commentService.RemoveComment(ctx, req)
handler.HandleResponse(ctx, err, nil)
}
@ -93,12 +110,17 @@ func (cc *CommentController) UpdateComment(ctx *gin.Context) {
}
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
if can, err := cc.rankService.CheckRankPermission(ctx, req.UserID, rank.CommentEditRank); err != nil || !can {
handler.HandleResponse(ctx, err, errors.Forbidden(reason.RankFailToMeetTheCondition))
can, err := cc.rankService.CheckOperationPermission(ctx, req.UserID, rank.CommentEditRank, req.CommentID)
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
}
if !can {
handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil)
return
}
err := cc.commentService.UpdateComment(ctx, req)
err = cc.commentService.UpdateComment(ctx, req)
handler.HandleResponse(ctx, err, nil)
}
@ -120,6 +142,16 @@ func (cc *CommentController) GetCommentWithPage(ctx *gin.Context) {
}
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
canList, err := cc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{
rank.CommentEditRank,
rank.CommentDeleteRank,
}, "")
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
}
req.CanEdit = canList[0]
req.CanDelete = canList[1]
resp, err := cc.commentService.GetCommentWithPage(ctx, req)
handler.HandleResponse(ctx, err, resp)
@ -162,6 +194,16 @@ func (cc *CommentController) GetComment(ctx *gin.Context) {
}
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
canList, err := cc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{
rank.CommentEditRank,
rank.CommentDeleteRank,
}, "")
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
}
req.CanEdit = canList[0]
req.CanDelete = canList[1]
resp, err := cc.commentService.GetComment(ctx, req)
handler.HandleResponse(ctx, err, resp)

View File

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

View File

@ -5,17 +5,25 @@ import (
"github.com/answerdev/answer/internal/base/middleware"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/notification"
"github.com/answerdev/answer/internal/service/rank"
"github.com/gin-gonic/gin"
)
// NotificationController notification controller
type NotificationController struct {
notificationService *notification.NotificationService
rankService *rank.RankService
}
// NewNotificationController new controller
func NewNotificationController(notificationService *notification.NotificationService) *NotificationController {
return &NotificationController{notificationService: notificationService}
func NewNotificationController(
notificationService *notification.NotificationService,
rankService *rank.RankService,
) *NotificationController {
return &NotificationController{
notificationService: notificationService,
rankService: rankService,
}
}
// GetRedDot
@ -28,8 +36,26 @@ func NewNotificationController(notificationService *notification.NotificationSer
// @Success 200 {object} handler.RespBody
// @Router /answer/api/v1/notification/status [get]
func (nc *NotificationController) GetRedDot(ctx *gin.Context) {
req := &schema.GetRedDot{}
userID := middleware.GetLoginUserIDFromContext(ctx)
RedDot, err := nc.notificationService.GetRedDot(ctx, userID)
req.UserID = userID
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
canList, err := nc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{
rank.QuestionAuditRank,
rank.AnswerAuditRank,
rank.TagAuditRank,
}, "")
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
}
req.CanReviewQuestion = canList[0]
req.CanReviewAnswer = canList[1]
req.CanReviewTag = canList[2]
RedDot, err := nc.notificationService.GetRedDot(ctx, req)
handler.HandleResponse(ctx, err, RedDot)
}
@ -48,8 +74,21 @@ func (nc *NotificationController) ClearRedDot(ctx *gin.Context) {
if handler.BindAndCheck(ctx, req) {
return
}
userID := middleware.GetLoginUserIDFromContext(ctx)
RedDot, err := nc.notificationService.ClearRedDot(ctx, userID, req.TypeStr)
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
canList, err := nc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{
rank.QuestionAuditRank,
rank.AnswerAuditRank,
rank.TagAuditRank,
}, "")
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
}
req.CanReviewQuestion = canList[0]
req.CanReviewAnswer = canList[1]
req.CanReviewTag = canList[2]
RedDot, err := nc.notificationService.ClearRedDot(ctx, req)
handler.HandleResponse(ctx, err, RedDot)
}

View File

@ -42,12 +42,18 @@ func (qc *QuestionController) RemoveQuestion(ctx *gin.Context) {
return
}
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
if can, err := qc.rankService.CheckRankPermission(ctx, req.UserID, rank.QuestionDeleteRank); err != nil || !can {
handler.HandleResponse(ctx, err, errors.Forbidden(reason.RankFailToMeetTheCondition))
req.IsAdmin = middleware.GetIsAdminFromContext(ctx)
can, err := qc.rankService.CheckOperationPermission(ctx, req.UserID, rank.QuestionDeleteRank, req.ID)
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
}
if !can {
handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil)
return
}
err := qc.questionService.RemoveQuestion(ctx, req)
err = qc.questionService.RemoveQuestion(ctx, req)
handler.HandleResponse(ctx, err, nil)
}
@ -67,6 +73,7 @@ func (qc *QuestionController) CloseQuestion(ctx *gin.Context) {
return
}
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
req.IsAdmin = middleware.GetIsAdminFromContext(ctx)
err := qc.questionService.CloseQuestion(ctx, req)
handler.HandleResponse(ctx, err, nil)
}
@ -81,16 +88,28 @@ func (qc *QuestionController) CloseQuestion(ctx *gin.Context) {
// @Param id query string true "Question TagID" default(1)
// @Success 200 {string} string ""
// @Router /answer/api/v1/question/info [get]
func (qc *QuestionController) GetQuestion(c *gin.Context) {
id := c.Query("id")
ctx := context.Background()
userID := middleware.GetLoginUserIDFromContext(c)
info, err := qc.questionService.GetQuestion(ctx, id, userID, true)
func (qc *QuestionController) GetQuestion(ctx *gin.Context) {
id := ctx.Query("id")
userID := middleware.GetLoginUserIDFromContext(ctx)
req := schema.QuestionPermission{}
canList, err := qc.rankService.CheckOperationPermissions(ctx, userID, []string{
rank.QuestionEditRank,
rank.QuestionDeleteRank,
}, id)
if err != nil {
handler.HandleResponse(c, err, nil)
handler.HandleResponse(ctx, err, nil)
return
}
handler.HandleResponse(c, nil, info)
req.CanEdit = canList[0]
req.CanDelete = canList[1]
req.CanClose = middleware.GetIsAdminFromContext(ctx)
info, err := qc.questionService.GetQuestionAndAddPV(ctx, id, userID, req)
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
}
handler.HandleResponse(ctx, nil, info)
}
// SimilarQuestion godoc
@ -188,8 +207,21 @@ func (qc *QuestionController) AddQuestion(ctx *gin.Context) {
}
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
if can, err := qc.rankService.CheckRankPermission(ctx, req.UserID, rank.QuestionAddRank); err != nil || !can {
handler.HandleResponse(ctx, err, errors.Forbidden(reason.RankFailToMeetTheCondition))
canList, err := qc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{
rank.QuestionAddRank,
rank.QuestionEditRank,
rank.QuestionDeleteRank,
}, "")
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
}
req.CanAdd = canList[0]
req.CanEdit = canList[1]
req.CanDelete = canList[2]
req.CanClose = middleware.GetIsAdminFromContext(ctx)
if !req.CanAdd {
handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil)
return
}
@ -214,13 +246,28 @@ func (qc *QuestionController) UpdateQuestion(ctx *gin.Context) {
}
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
if can, err := qc.rankService.CheckRankPermission(ctx, req.UserID, rank.QuestionEditRank); err != nil || !can {
handler.HandleResponse(ctx, err, errors.Forbidden(reason.RankFailToMeetTheCondition))
canList, err := qc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{
rank.QuestionEditRank,
rank.QuestionDeleteRank,
rank.QuestionEditWithoutReviewRank,
}, req.ID)
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
}
req.CanEdit = canList[0]
req.CanDelete = canList[1]
req.NoNeedReview = canList[2]
req.CanClose = middleware.GetIsAdminFromContext(ctx)
req.IsAdmin = middleware.GetIsAdminFromContext(ctx)
if !req.CanEdit {
handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil)
return
}
resp, err := qc.questionService.UpdateQuestion(ctx, req)
handler.HandleResponse(ctx, err, resp)
_, err = qc.questionService.UpdateQuestion(ctx, req)
handler.HandleResponse(ctx, err, &schema.UpdateQuestionResp{WaitForReview: !req.NoNeedReview})
}
// CloseMsgList close question msg list

View File

@ -40,12 +40,17 @@ func (rc *ReportController) AddReport(ctx *gin.Context) {
}
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
if can, err := rc.rankService.CheckRankPermission(ctx, req.UserID, rank.ReportAddRank); err != nil || !can {
handler.HandleResponse(ctx, err, errors.Forbidden(reason.RankFailToMeetTheCondition))
can, err := rc.rankService.CheckOperationPermission(ctx, req.UserID, rank.ReportAddRank, "")
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
}
if !can {
handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil)
return
}
err := rc.reportService.AddReport(ctx, req)
err = rc.reportService.AddReport(ctx, req)
handler.HandleResponse(ctx, err, nil)
}

View File

@ -1,10 +1,14 @@
package controller
import (
"github.com/answerdev/answer/internal/base/constant"
"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/schema"
"github.com/answerdev/answer/internal/service"
"github.com/answerdev/answer/internal/service/rank"
"github.com/answerdev/answer/pkg/obj"
"github.com/gin-gonic/gin"
"github.com/segmentfault/pacman/errors"
)
@ -12,11 +16,18 @@ import (
// RevisionController revision controller
type RevisionController struct {
revisionListService *service.RevisionService
rankService *rank.RankService
}
// NewRevisionController new controller
func NewRevisionController(revisionListService *service.RevisionService) *RevisionController {
return &RevisionController{revisionListService: revisionListService}
func NewRevisionController(
revisionListService *service.RevisionService,
rankService *rank.RankService,
) *RevisionController {
return &RevisionController{
revisionListService: revisionListService,
rankService: rankService,
}
}
// GetRevisionList godoc
@ -41,3 +52,113 @@ func (rc *RevisionController) GetRevisionList(ctx *gin.Context) {
resp, err := rc.revisionListService.GetRevisionList(ctx, req)
handler.HandleResponse(ctx, err, resp)
}
// GetUnreviewedRevisionList godoc
// @Summary get unreviewed revision list
// @Description get unreviewed revision list
// @Tags Revision
// @Produce json
// @Security ApiKeyAuth
// @Param page query string true "page id"
// @Success 200 {object} handler.RespBody{data=pager.PageModel{list=[]schema.GetUnreviewedRevisionResp}}
// @Router /answer/api/v1/revisions/unreviewed [get]
func (rc *RevisionController) GetUnreviewedRevisionList(ctx *gin.Context) {
req := &schema.RevisionSearch{}
if handler.BindAndCheck(ctx, req) {
return
}
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
canList, err := rc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{
rank.QuestionAuditRank,
rank.AnswerAuditRank,
rank.TagAuditRank,
}, "")
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
}
req.CanReviewQuestion = canList[0]
req.CanReviewAnswer = canList[1]
req.CanReviewTag = canList[2]
resp, err := rc.revisionListService.GetUnreviewedRevisionPage(ctx, req)
handler.HandleResponse(ctx, err, resp)
}
// RevisionAudit godoc
// @Summary revision audit
// @Description revision audit operation:approve or reject
// @Tags Revision
// @Produce json
// @Security ApiKeyAuth
// @Param data body schema.RevisionAuditReq true "audit"
// @Success 200 {object} handler.RespBody{}
// @Router /answer/api/v1/revisions/audit [put]
func (rc *RevisionController) RevisionAudit(ctx *gin.Context) {
req := &schema.RevisionAuditReq{}
if handler.BindAndCheck(ctx, req) {
return
}
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
canList, err := rc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{
rank.QuestionAuditRank,
rank.AnswerAuditRank,
rank.TagAuditRank,
}, "")
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
}
req.CanReviewQuestion = canList[0]
req.CanReviewAnswer = canList[1]
req.CanReviewTag = canList[2]
err = rc.revisionListService.RevisionAudit(ctx, req)
handler.HandleResponse(ctx, err, gin.H{})
}
// CheckCanUpdateRevision check can update revision
// @Summary check can update revision
// @Description check can update revision
// @Tags Revision
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param id query string true "id" default(string)
// @Success 200 {object} handler.RespBody
// @Router /answer/api/v1/revisions/edit/check [get]
func (rc *RevisionController) CheckCanUpdateRevision(ctx *gin.Context) {
req := &schema.CheckCanQuestionUpdate{}
if handler.BindAndCheck(ctx, req) {
return
}
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
action := ""
objectTypeStr, _ := obj.GetObjectTypeStrByObjectID(req.ID)
switch objectTypeStr {
case constant.QuestionObjectType:
action = rank.QuestionEditRank
case constant.AnswerObjectType:
action = rank.AnswerEditRank
case constant.TagObjectType:
action = rank.TagEditRank
default:
handler.HandleResponse(ctx, errors.BadRequest(reason.ObjectNotFound), nil)
return
}
can, err := rc.rankService.CheckOperationPermission(ctx, req.UserID, action, req.ID)
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
}
if !can {
handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil)
return
}
resp, err := rc.revisionListService.CheckCanUpdateRevision(ctx, req)
handler.HandleResponse(ctx, err, resp)
}

View File

@ -42,8 +42,7 @@ func (tc *TagController) SearchTagLike(ctx *gin.Context) {
if handler.BindAndCheck(ctx, req) {
return
}
userinfo := middleware.GetUserInfoFromContext(ctx)
req.IsAdmin = userinfo.IsAdmin
req.IsAdmin = middleware.GetIsAdminFromContext(ctx)
resp, err := tc.tagCommonService.SearchTagLike(ctx, req)
handler.HandleResponse(ctx, err, resp)
}
@ -64,12 +63,17 @@ func (tc *TagController) RemoveTag(ctx *gin.Context) {
}
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
if can, err := tc.rankService.CheckRankPermission(ctx, req.UserID, rank.TagDeleteRank); err != nil || !can {
handler.HandleResponse(ctx, err, errors.Forbidden(reason.RankFailToMeetTheCondition))
can, err := tc.rankService.CheckOperationPermission(ctx, req.UserID, rank.TagDeleteRank, "")
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
}
if !can {
handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil)
return
}
err := tc.tagService.RemoveTag(ctx, req.TagID)
err = tc.tagService.RemoveTag(ctx, req)
handler.HandleResponse(ctx, err, nil)
}
@ -89,13 +93,26 @@ func (tc *TagController) UpdateTag(ctx *gin.Context) {
}
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
if can, err := tc.rankService.CheckRankPermission(ctx, req.UserID, rank.TagEditRank); err != nil || !can {
handler.HandleResponse(ctx, err, errors.Forbidden(reason.RankFailToMeetTheCondition))
canList, err := tc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{
rank.TagEditRank,
rank.TagEditWithoutReviewRank,
}, "")
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
}
if !canList[0] {
handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil)
return
}
req.NoNeedReview = canList[1]
err := tc.tagService.UpdateTag(ctx, req)
handler.HandleResponse(ctx, err, nil)
err = tc.tagService.UpdateTag(ctx, req)
if err != nil {
handler.HandleResponse(ctx, err, nil)
} else {
handler.HandleResponse(ctx, err, &schema.UpdateTagResp{WaitForReview: !req.NoNeedReview})
}
}
// GetTagInfo get tag one
@ -115,6 +132,16 @@ func (tc *TagController) GetTagInfo(ctx *gin.Context) {
}
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
canList, err := tc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{
rank.TagEditRank,
rank.TagDeleteRank,
}, "")
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
}
req.CanEdit = canList[0]
req.CanDelete = canList[1]
resp, err := tc.tagService.GetTagInfo(ctx, req)
handler.HandleResponse(ctx, err, resp)
@ -163,7 +190,7 @@ func (tc *TagController) GetFollowingTags(ctx *gin.Context) {
// @Tags Tag
// @Produce json
// @Param tag_id query int true "tag id"
// @Success 200 {object} handler.RespBody{data=[]schema.GetTagSynonymsResp}
// @Success 200 {object} handler.RespBody{data=schema.GetTagSynonymsResp}
// @Router /answer/api/v1/tag/synonyms [get]
func (tc *TagController) GetTagSynonyms(ctx *gin.Context) {
req := &schema.GetTagSynonymsReq{}
@ -171,6 +198,16 @@ func (tc *TagController) GetTagSynonyms(ctx *gin.Context) {
return
}
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
canList, err := tc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{
rank.TagSynonymRank,
}, "")
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
}
req.CanEdit = canList[0]
resp, err := tc.tagService.GetTagSynonyms(ctx, req)
handler.HandleResponse(ctx, err, resp)
}
@ -191,11 +228,16 @@ func (tc *TagController) UpdateTagSynonym(ctx *gin.Context) {
}
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
if can, err := tc.rankService.CheckRankPermission(ctx, req.UserID, rank.TagSynonymRank); err != nil || !can {
handler.HandleResponse(ctx, err, errors.Forbidden(reason.RankFailToMeetTheCondition))
can, err := tc.rankService.CheckOperationPermission(ctx, req.UserID, rank.TagSynonymRank, "")
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
}
if !can {
handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil)
return
}
err := tc.tagService.UpdateTagSynonym(ctx, req)
err = tc.tagService.UpdateTagSynonym(ctx, req)
handler.HandleResponse(ctx, err, nil)
}

View File

@ -441,9 +441,6 @@ func (uc *UserController) UserChangeEmailSendCode(ctx *gin.Context) {
_, _ = uc.actionService.ActionRecordAdd(ctx, schema.ActionRecordTypeEmail, ctx.ClientIP())
resp, err := uc.userService.UserChangeEmailSendCode(ctx, req)
if err != nil {
if resp != nil {
resp.ErrorMsg = translator.GlobalTrans.Tr(handler.GetLang(ctx), resp.ErrorMsg)
}
handler.HandleResponse(ctx, err, resp)
return
}

View File

@ -3,20 +3,24 @@ package controller
import (
"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/schema"
"github.com/answerdev/answer/internal/service"
"github.com/answerdev/answer/internal/service/rank"
"github.com/gin-gonic/gin"
"github.com/jinzhu/copier"
"github.com/segmentfault/pacman/errors"
)
// VoteController activity controller
type VoteController struct {
VoteService *service.VoteService
rankService *rank.RankService
}
// NewVoteController new controller
func NewVoteController(voteService *service.VoteService) *VoteController {
return &VoteController{VoteService: voteService}
func NewVoteController(voteService *service.VoteService, rankService *rank.RankService) *VoteController {
return &VoteController{VoteService: voteService, rankService: rankService}
}
// VoteUp godoc
@ -34,9 +38,19 @@ func (vc *VoteController) VoteUp(ctx *gin.Context) {
if handler.BindAndCheck(ctx, req) {
return
}
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
can, err := vc.rankService.CheckVotePermission(ctx, req.UserID, req.ObjectID, true)
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
}
if !can {
handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil)
return
}
dto := &schema.VoteDTO{}
_ = copier.Copy(dto, req)
dto.UserID = middleware.GetLoginUserIDFromContext(ctx)
resp, err := vc.VoteService.VoteUp(ctx, dto)
if err != nil {
handler.HandleResponse(ctx, err, schema.ErrTypeToast)
@ -60,10 +74,20 @@ func (vc *VoteController) VoteDown(ctx *gin.Context) {
if handler.BindAndCheck(ctx, req) {
return
}
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
can, err := vc.rankService.CheckVotePermission(ctx, req.UserID, req.ObjectID, false)
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
}
if !can {
handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil)
return
}
dto := &schema.VoteDTO{}
_ = copier.Copy(dto, req)
dto.UserID = middleware.GetLoginUserIDFromContext(ctx)
resp, err := vc.VoteService.VoteDown(ctx, dto)
if err != nil {
handler.HandleResponse(ctx, err, schema.ErrTypeToast)

View File

@ -9,16 +9,19 @@ const (
// Activity activity
type Activity struct {
ID string `xorm:"not null pk autoincr BIGINT(20) id"`
CreatedAt time.Time `xorm:"created TIMESTAMP created_at"`
UpdatedAt time.Time `xorm:"updated TIMESTAMP updated_at"`
UserID string `xorm:"not null index BIGINT(20) user_id"`
TriggerUserID int64 `xorm:"not null default 0 index BIGINT(20) trigger_user_id"`
ObjectID string `xorm:"not null default 0 index BIGINT(20) object_id"`
ActivityType int `xorm:"not null INT(11) activity_type"`
Cancelled int `xorm:"not null default 0 TINYINT(4) cancelled"`
Rank int `xorm:"not null default 0 INT(11) rank"`
HasRank int `xorm:"not null default 0 TINYINT(4) has_rank"`
ID string `xorm:"not null pk autoincr BIGINT(20) id"`
CreatedAt time.Time `xorm:"created TIMESTAMP created_at"`
UpdatedAt time.Time `xorm:"updated TIMESTAMP updated_at"`
CancelledAt time.Time `xorm:"TIMESTAMP cancelled_at"`
UserID string `xorm:"not null index BIGINT(20) user_id"`
TriggerUserID int64 `xorm:"not null default 0 index BIGINT(20) trigger_user_id"`
ObjectID string `xorm:"not null default 0 index BIGINT(20) object_id"`
OriginalObjectID string `xorm:"not null default 0 BIGINT(20) original_object_id"`
ActivityType int `xorm:"not null INT(11) activity_type"`
Cancelled int `xorm:"not null default 0 TINYINT(4) cancelled"`
Rank int `xorm:"not null default 0 INT(11) rank"`
HasRank int `xorm:"not null default 0 TINYINT(4) has_rank"`
RevisionID int64 `xorm:"not null default 0 BIGINT(20) revision_id"`
}
type ActivityRankSum struct {

View File

@ -18,18 +18,19 @@ var CmsAnswerSearchStatus = map[string]int{
// Answer answer
type Answer struct {
ID string `xorm:"not null pk autoincr BIGINT(20) id"`
CreatedAt time.Time `xorm:"created not null default CURRENT_TIMESTAMP TIMESTAMP created_at"`
UpdatedAt time.Time `xorm:"not null default CURRENT_TIMESTAMP TIMESTAMP updated_at"`
QuestionID string `xorm:"not null default 0 BIGINT(20) question_id"`
UserID string `xorm:"not null default 0 BIGINT(20) INDEX user_id"`
OriginalText string `xorm:"not null MEDIUMTEXT original_text"`
ParsedText string `xorm:"not null MEDIUMTEXT parsed_text"`
Status int `xorm:"not null default 1 INT(11) status"`
Adopted int `xorm:"not null default 1 INT(11) adopted"`
CommentCount int `xorm:"not null default 0 INT(11) comment_count"`
VoteCount int `xorm:"not null default 0 INT(11) vote_count"`
RevisionID string `xorm:"not null default 0 BIGINT(20) revision_id"`
ID string `xorm:"not null pk autoincr BIGINT(20) id"`
CreatedAt time.Time `xorm:"created not null default CURRENT_TIMESTAMP TIMESTAMP created_at"`
UpdatedAt time.Time `xorm:"updated_at TIMESTAMP"`
QuestionID string `xorm:"not null default 0 BIGINT(20) question_id"`
UserID string `xorm:"not null default 0 BIGINT(20) INDEX user_id"`
LastEditUserID string `xorm:"not null default 0 BIGINT(20) last_edit_user_id"`
OriginalText string `xorm:"not null MEDIUMTEXT original_text"`
ParsedText string `xorm:"not null MEDIUMTEXT parsed_text"`
Status int `xorm:"not null default 1 INT(11) status"`
Adopted int `xorm:"not null default 1 INT(11) adopted"`
CommentCount int `xorm:"not null default 0 INT(11) comment_count"`
VoteCount int `xorm:"not null default 0 INT(11) vote_count"`
RevisionID string `xorm:"not null default 0 BIGINT(20) revision_id"`
}
type AnswerSearch struct {
@ -48,11 +49,6 @@ type CmsAnswerSearch struct {
QuestionID string `validate:"omitempty,gt=0,lte=24" json:"question_id" form:"question_id" ` //Query string
}
type AdminSetAnswerStatusRequest struct {
StatusStr string `json:"status" form:"status"`
AnswerID string `json:"answer_id" form:"answer_id"`
}
// TableName answer table name
func (Answer) TableName() string {
return "answer"

View File

@ -3,7 +3,7 @@ package entity
// Config config
type Config struct {
ID int `xorm:"not null pk autoincr INT(11) id"`
Key string `xorm:"unique VARCHAR(32) key"`
Key string `xorm:"unique VARCHAR(128) key"`
Value string `xorm:"TEXT value"`
}

View File

@ -6,19 +6,19 @@ import (
const (
QuestionStatusAvailable = 1
QuestionStatusclosed = 2
QuestionStatusClosed = 2
QuestionStatusDeleted = 10
)
var CmsQuestionSearchStatus = map[string]int{
"available": QuestionStatusAvailable,
"closed": QuestionStatusclosed,
"closed": QuestionStatusClosed,
"deleted": QuestionStatusDeleted,
}
var CmsQuestionSearchStatusIntToString = map[int]string{
QuestionStatusAvailable: "available",
QuestionStatusclosed: "closed",
QuestionStatusClosed: "closed",
QuestionStatusDeleted: "deleted",
}
@ -31,8 +31,9 @@ type QuestionTag struct {
type Question struct {
ID string `xorm:"not null pk BIGINT(20) id"`
CreatedAt time.Time `xorm:"not null default CURRENT_TIMESTAMP TIMESTAMP created_at"`
UpdatedAt time.Time `xorm:"not null default CURRENT_TIMESTAMP TIMESTAMP updated_at"`
UpdatedAt time.Time `xorm:"updated_at TIMESTAMP"`
UserID string `xorm:"not null default 0 BIGINT(20) INDEX user_id"`
LastEditUserID string `xorm:"not null default 0 BIGINT(20) last_edit_user_id"`
Title string `xorm:"not null default '' VARCHAR(150) title"`
OriginalText string `xorm:"not null MEDIUMTEXT original_text"`
ParsedText string `xorm:"not null MEDIUMTEXT parsed_text"`
@ -45,11 +46,29 @@ type Question struct {
FollowCount int `xorm:"not null default 0 INT(11) follow_count"`
AcceptedAnswerID string `xorm:"not null default 0 BIGINT(20) accepted_answer_id"`
LastAnswerID string `xorm:"not null default 0 BIGINT(20) last_answer_id"`
PostUpdateTime time.Time `xorm:"default CURRENT_TIMESTAMP TIMESTAMP post_update_time"`
RevisionID string `xorm:"not null default 0 BIGINT(20) revision_id"`
PostUpdateTime time.Time `xorm:"post_update_time TIMESTAMP"`
RevisionID string `xorm:"not null default 0 BIGINT(20) revision_id"`
}
// TableName question table name
func (Question) TableName() string {
return "question"
}
// QuestionWithTagsRevision question
type QuestionWithTagsRevision struct {
Question
Tags []*TagSimpleInfoForRevision `json:"tags"`
}
// TagSimpleInfoForRevision tag simple info for revision
type TagSimpleInfoForRevision struct {
ID string `xorm:"not null pk comment('tag_id') BIGINT(20) id"`
MainTagID int64 `xorm:"not null default 0 BIGINT(20) main_tag_id"`
MainTagSlugName string `xorm:"not null default '' VARCHAR(35) main_tag_slug_name"`
SlugName string `xorm:"not null default '' unique VARCHAR(35) slug_name"`
DisplayName string `xorm:"not null default '' VARCHAR(35) display_name"`
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

@ -1,20 +1,31 @@
package entity
import "time"
import (
"time"
)
const (
// RevisionUnreviewedStatus this revision is unreviewed
RevisionUnreviewedStatus = 1
// RevisionReviewPassStatus this revision is reviewed and approved by operator
RevisionReviewPassStatus = 2
// RevisionReviewRejectStatus this revision is reviewed and rejected by operator
RevisionReviewRejectStatus = 3
)
// Revision revision
type Revision struct {
ID string `xorm:"not null pk autoincr BIGINT(20) id"`
CreatedAt time.Time `xorm:"created TIMESTAMP created_at"`
UpdatedAt time.Time `xorm:"updated TIMESTAMP updated_at"`
UserID string `xorm:"not null default 0 BIGINT(20) user_id"`
ObjectType int `xorm:"not null default 0 ) INT(11) object_type"`
ObjectID string `xorm:"not null default 0 BIGINT(20) INDEX object_id"`
Title string `xorm:"not null default '' VARCHAR(255) title"`
Content string `xorm:"not null TEXT content"`
Log string `xorm:"VARCHAR(255) log"`
// Status todo: this field is not used, will be removed in the future
Status int `xorm:"not null default 1 INT(11) status"`
ID string `xorm:"not null pk autoincr BIGINT(20) id"`
CreatedAt time.Time `xorm:"created TIMESTAMP created_at"`
UpdatedAt time.Time `xorm:"updated TIMESTAMP updated_at"`
UserID string `xorm:"not null default 0 BIGINT(20) user_id"`
ObjectType int `xorm:"not null default 0 INT(11) object_type"`
ObjectID string `xorm:"not null default 0 BIGINT(20) INDEX object_id"`
Title string `xorm:"not null default '' VARCHAR(255) title"`
Content string `xorm:"not null TEXT content"`
Log string `xorm:"VARCHAR(255) log"`
Status int `xorm:"not null default 1 INT(11) status"`
ReviewUserID int64 `xorm:"not null default 0 BIGINT(20) review_user_id"`
}
// TableName revision table name

View File

@ -24,6 +24,7 @@ type Tag struct {
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"`
UserID string `xorm:"not null default 0 BIGINT(20) user_id"`
}
// TableName tag table name

View File

@ -163,7 +163,7 @@ func InitBaseInfo(ctx *gin.Context) {
}
if cli.CheckDBTableExist(c.Data.Database) {
log.Warnf("database is already initialized")
log.Warn("database is already initialized")
handler.HandleResponse(ctx, nil, nil)
return
}

View File

@ -242,6 +242,26 @@ func initConfigTable(engine *xorm.Engine) error {
{ID: 84, Key: "question.review.reasons", Value: `["reason.looks_ok","reason.needs_edit","reason.needs_close","reason.needs_delete"]`},
{ID: 85, Key: "answer.review.reasons", Value: `["reason.looks_ok","reason.needs_edit","reason.needs_delete"]`},
{ID: 86, Key: "comment.review.reasons", Value: `["reason.looks_ok","reason.needs_edit","reason.needs_delete"]`},
{ID: 87, Key: "question.asked", Value: `0`},
{ID: 88, Key: "question.closed", Value: `0`},
{ID: 89, Key: "question.reopened", Value: `0`},
{ID: 90, Key: "question.answered", Value: `0`},
{ID: 91, Key: "question.commented", Value: `0`},
{ID: 92, Key: "question.accept", Value: `0`},
{ID: 93, Key: "question.edited", Value: `0`},
{ID: 94, Key: "question.rollback", Value: `0`},
{ID: 95, Key: "question.deleted", Value: `0`},
{ID: 96, Key: "question.undeleted", Value: `0`},
{ID: 97, Key: "answer.answered", Value: `0`},
{ID: 98, Key: "answer.commented", Value: `0`},
{ID: 99, Key: "answer.edited", Value: `0`},
{ID: 100, Key: "answer.rollback", Value: `0`},
{ID: 101, Key: "answer.undeleted", Value: `0`},
{ID: 102, Key: "tag.created", Value: `0`},
{ID: 103, Key: "tag.edited", Value: `0`},
{ID: 104, Key: "tag.rollback", Value: `0`},
{ID: 105, Key: "tag.deleted", Value: `0`},
{ID: 106, Key: "tag.undeleted", Value: `0`},
}
_, err := engine.Insert(defaultConfigTable)
return err

View File

@ -44,6 +44,7 @@ var migrations = []Migration{
NewMigration("this is first version, no operation", noopMigration),
NewMigration("add user language", addUserLanguage),
NewMigration("add recommend and reserved tag fields", addTagRecommendedAndReserved),
NewMigration("add activity timeline", addActivityTimeline),
}
// GetCurrentDBVersion returns the current db version

200
internal/migrations/v3.go Normal file
View File

@ -0,0 +1,200 @@
package migrations
import (
"fmt"
"time"
"github.com/answerdev/answer/internal/entity"
"github.com/segmentfault/pacman/log"
"xorm.io/xorm"
"xorm.io/xorm/schemas"
)
func addActivityTimeline(x *xorm.Engine) (err error) {
switch x.Dialect().URI().DBType {
case schemas.MYSQL:
_, err = x.Exec("ALTER TABLE `answer` CHANGE `updated_at` `updated_at` TIMESTAMP NULL DEFAULT NULL")
if err != nil {
return err
}
_, err = x.Exec("ALTER TABLE `question` CHANGE `updated_at` `updated_at` TIMESTAMP NULL DEFAULT NULL")
if err != nil {
return err
}
case schemas.POSTGRES:
_, err = x.Exec(`ALTER TABLE "answer" ALTER COLUMN "updated_at" DROP NOT NULL, ALTER COLUMN "updated_at" SET DEFAULT NULL`)
if err != nil {
return err
}
_, err = x.Exec(`ALTER TABLE "question" ALTER COLUMN "updated_at" DROP NOT NULL, ALTER COLUMN "updated_at" SET DEFAULT NULL`)
if err != nil {
return err
}
case schemas.SQLITE:
_, err = x.Exec(`DROP INDEX "IDX_answer_user_id";
ALTER TABLE "answer" RENAME TO "_answer_old_v3";
CREATE TABLE "answer" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" DATETIME DEFAULT NULL,
"question_id" INTEGER NOT NULL DEFAULT 0,
"user_id" INTEGER NOT NULL DEFAULT 0,
"original_text" TEXT NOT NULL,
"parsed_text" TEXT NOT NULL,
"status" INTEGER NOT NULL DEFAULT 1,
"adopted" INTEGER NOT NULL DEFAULT 1,
"comment_count" INTEGER NOT NULL DEFAULT 0,
"vote_count" INTEGER NOT NULL DEFAULT 0,
"revision_id" INTEGER NOT NULL DEFAULT 0
);
INSERT INTO "answer" ("id", "created_at", "updated_at", "question_id", "user_id", "original_text", "parsed_text", "status", "adopted", "comment_count", "vote_count", "revision_id") SELECT "id", "created_at", "updated_at", "question_id", "user_id", "original_text", "parsed_text", "status", "adopted", "comment_count", "vote_count", "revision_id" FROM "_answer_old_v3";
CREATE INDEX "IDX_answer_user_id"
ON "answer" (
"user_id" ASC
);
DROP INDEX "IDX_question_user_id";
ALTER TABLE "question" RENAME TO "_question_old_v3";
CREATE TABLE "question" (
"id" INTEGER NOT NULL,
"created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" DATETIME DEFAULT NULL,
"user_id" INTEGER NOT NULL DEFAULT 0,
"title" TEXT NOT NULL DEFAULT '',
"original_text" TEXT NOT NULL,
"parsed_text" TEXT NOT NULL,
"status" INTEGER NOT NULL DEFAULT 1,
"view_count" INTEGER NOT NULL DEFAULT 0,
"unique_view_count" INTEGER NOT NULL DEFAULT 0,
"vote_count" INTEGER NOT NULL DEFAULT 0,
"answer_count" INTEGER NOT NULL DEFAULT 0,
"collection_count" INTEGER NOT NULL DEFAULT 0,
"follow_count" INTEGER NOT NULL DEFAULT 0,
"accepted_answer_id" INTEGER NOT NULL DEFAULT 0,
"last_answer_id" INTEGER NOT NULL DEFAULT 0,
"post_update_time" DATETIME DEFAULT CURRENT_TIMESTAMP,
"revision_id" INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY ("id")
);
INSERT INTO "question" ("id", "created_at", "updated_at", "user_id", "title", "original_text", "parsed_text", "status", "view_count", "unique_view_count", "vote_count", "answer_count", "collection_count", "follow_count", "accepted_answer_id", "last_answer_id", "post_update_time", "revision_id") SELECT "id", "created_at", "updated_at", "user_id", "title", "original_text", "parsed_text", "status", "view_count", "unique_view_count", "vote_count", "answer_count", "collection_count", "follow_count", "accepted_answer_id", "last_answer_id", "post_update_time", "revision_id" FROM "_question_old_v3";
CREATE INDEX "IDX_question_user_id"
ON "question" (
"user_id" ASC
);`)
if err != nil {
return err
}
}
// only increasing field length to 128
type Config struct {
Key string `xorm:"unique VARCHAR(128) key"`
}
if err := x.Sync(new(Config)); err != nil {
return fmt.Errorf("sync config table failed: %w", err)
}
defaultConfigTable := []*entity.Config{
{ID: 36, Key: "rank.question.add", Value: `1`},
{ID: 37, Key: "rank.question.edit", Value: `200`},
{ID: 38, Key: "rank.question.delete", Value: `-1`},
{ID: 39, Key: "rank.question.vote_up", Value: `15`},
{ID: 40, Key: "rank.question.vote_down", Value: `125`},
{ID: 41, Key: "rank.answer.add", Value: `1`},
{ID: 42, Key: "rank.answer.edit", Value: `200`},
{ID: 43, Key: "rank.answer.delete", Value: `-1`},
{ID: 44, Key: "rank.answer.accept", Value: `1`},
{ID: 45, Key: "rank.answer.vote_up", Value: `15`},
{ID: 46, Key: "rank.answer.vote_down", Value: `125`},
{ID: 47, Key: "rank.comment.add", Value: `1`},
{ID: 48, Key: "rank.comment.edit", Value: `-1`},
{ID: 49, Key: "rank.comment.delete", Value: `-1`},
{ID: 50, Key: "rank.report.add", Value: `1`},
{ID: 51, Key: "rank.tag.add", Value: `1`},
{ID: 52, Key: "rank.tag.edit", Value: `100`},
{ID: 53, Key: "rank.tag.delete", Value: `-1`},
{ID: 54, Key: "rank.tag.synonym", Value: `20000`},
{ID: 55, Key: "rank.link.url_limit", Value: `10`},
{ID: 56, Key: "rank.vote.detail", Value: `0`},
{ID: 87, Key: "question.asked", Value: `0`},
{ID: 88, Key: "question.closed", Value: `0`},
{ID: 89, Key: "question.reopened", Value: `0`},
{ID: 90, Key: "question.answered", Value: `0`},
{ID: 91, Key: "question.commented", Value: `0`},
{ID: 92, Key: "question.accept", Value: `0`},
{ID: 93, Key: "question.edited", Value: `0`},
{ID: 94, Key: "question.rollback", Value: `0`},
{ID: 95, Key: "question.deleted", Value: `0`},
{ID: 96, Key: "question.undeleted", Value: `0`},
{ID: 97, Key: "answer.answered", Value: `0`},
{ID: 98, Key: "answer.commented", Value: `0`},
{ID: 99, Key: "answer.edited", Value: `0`},
{ID: 100, Key: "answer.rollback", Value: `0`},
{ID: 101, Key: "answer.undeleted", Value: `0`},
{ID: 102, Key: "tag.created", Value: `0`},
{ID: 103, Key: "tag.edited", Value: `0`},
{ID: 104, Key: "tag.rollback", Value: `0`},
{ID: 105, Key: "tag.deleted", Value: `0`},
{ID: 106, Key: "tag.undeleted", Value: `0`},
{ID: 107, Key: "rank.comment.vote_up", Value: `1`},
{ID: 108, Key: "rank.comment.vote_down", Value: `1`},
{ID: 109, Key: "rank.question.edit_without_review", Value: `2000`},
{ID: 110, Key: "rank.answer.edit_without_review", Value: `2000`},
{ID: 111, Key: "rank.tag.edit_without_review", Value: `20000`},
{ID: 112, Key: "rank.answer.audit", Value: `2000`},
{ID: 113, Key: "rank.question.audit", Value: `2000`},
{ID: 114, Key: "rank.tag.audit", Value: `20000`},
}
for _, c := range defaultConfigTable {
exist, err := x.Get(&entity.Config{ID: c.ID, Key: c.Key})
if err != nil {
return fmt.Errorf("get config failed: %w", err)
}
if exist {
if _, err = x.Update(c, &entity.Config{ID: c.ID, Key: c.Key}); err != nil {
log.Errorf("update %+v config failed: %s", c, err)
return fmt.Errorf("update config failed: %w", err)
}
continue
}
if _, err = x.Insert(&entity.Config{ID: c.ID, Key: c.Key, Value: c.Value}); err != nil {
log.Errorf("insert %+v config failed: %s", c, err)
return fmt.Errorf("add config failed: %w", err)
}
}
type Revision struct {
ReviewUserID int64 `xorm:"not null default 0 BIGINT(20) review_user_id"`
}
type Activity struct {
CancelledAt time.Time `xorm:"TIMESTAMP cancelled_at"`
RevisionID int64 `xorm:"not null default 0 BIGINT(20) revision_id"`
OriginalObjectID string `xorm:"not null default 0 BIGINT(20) original_object_id"`
}
type Tag struct {
UserID string `xorm:"not null default 0 BIGINT(20) user_id"`
}
type Question struct {
UpdatedAt time.Time `xorm:"updated_at TIMESTAMP"`
LastEditUserID string `xorm:"not null default 0 BIGINT(20) last_edit_user_id"`
PostUpdateTime time.Time `xorm:"post_update_time TIMESTAMP"`
}
type Answer struct {
UpdatedAt time.Time `xorm:"updated_at TIMESTAMP"`
LastEditUserID string `xorm:"not null default 0 BIGINT(20) last_edit_user_id"`
}
err = x.Sync(new(Activity), new(Revision), new(Tag), new(Question), new(Answer))
if err != nil {
return fmt.Errorf("sync table failed %w", err)
}
return nil
}

View File

@ -0,0 +1,54 @@
package activity
import (
"context"
"fmt"
"github.com/answerdev/answer/internal/base/constant"
"github.com/answerdev/answer/internal/base/data"
"github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/internal/entity"
"github.com/answerdev/answer/internal/repo/config"
"github.com/answerdev/answer/internal/service/activity"
"github.com/segmentfault/pacman/errors"
)
// activityRepo activity repository
type activityRepo struct {
data *data.Data
}
// NewActivityRepo new repository
func NewActivityRepo(
data *data.Data,
) activity.ActivityRepo {
return &activityRepo{
data: data,
}
}
func (ar *activityRepo) GetObjectAllActivity(ctx context.Context, objectID string, showVote bool) (
activityList []*entity.Activity, err error) {
activityList = make([]*entity.Activity, 0)
session := ar.data.DB.Desc("created_at")
if !showVote {
var activityTypeNotShown []int
for _, obj := range []string{constant.AnswerObjectType, constant.QuestionObjectType, constant.CommentObjectType} {
for _, act := range []string{
constant.ActVotedDown,
constant.ActVotedUp,
constant.ActVoteDown,
constant.ActVoteUp,
} {
activityTypeNotShown = append(activityTypeNotShown, config.Key2IDMapping[fmt.Sprintf("%s.%s", obj, act)])
}
}
session.NotIn("activity_type", activityTypeNotShown)
}
err = session.Find(&activityList, &entity.Activity{OriginalObjectID: objectID})
if err != nil {
return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return activityList, nil
}

View File

@ -2,6 +2,7 @@ package activity
import (
"context"
"time"
"github.com/answerdev/answer/internal/base/constant"
"github.com/answerdev/answer/internal/base/data"
@ -96,8 +97,8 @@ func (ar *AnswerActivityRepo) DeleteQuestion(ctx context.Context, questionID str
return nil, errors.InternalServer(reason.DatabaseError).WithError(e).WithStack()
}
if _, e := session.Where("id = ?", act.ID).Cols("`cancelled`").
Update(&entity.Activity{Cancelled: entity.ActivityCancelled}); e != nil {
if _, e := session.Where("id = ?", act.ID).Cols("cancelled", "cancelled_at").
Update(&entity.Activity{Cancelled: entity.ActivityCancelled, CancelledAt: time.Now()}); e != nil {
return nil, errors.InternalServer(reason.DatabaseError).WithError(e).WithStack()
}
}
@ -124,7 +125,7 @@ func (ar *AnswerActivityRepo) DeleteQuestion(ctx context.Context, questionID str
// AcceptAnswer accept other answer
func (ar *AnswerActivityRepo) AcceptAnswer(ctx context.Context,
answerObjID, questionUserID, answerUserID string, isSelf bool,
answerObjID, questionObjID, questionUserID, answerUserID string, isSelf bool,
) (err error) {
addActivityList := make([]*entity.Activity, 0)
for _, action := range acceptActionList {
@ -134,17 +135,20 @@ func (ar *AnswerActivityRepo) AcceptAnswer(ctx context.Context,
return errors.InternalServer(reason.DatabaseError).WithError(e).WithStack()
}
addActivity := &entity.Activity{
ObjectID: answerObjID,
ActivityType: activityType,
Rank: deltaRank,
HasRank: hasRank,
ObjectID: answerObjID,
OriginalObjectID: questionObjID,
ActivityType: activityType,
Rank: deltaRank,
HasRank: hasRank,
}
if action == acceptAction {
addActivity.UserID = questionUserID
addActivity.TriggerUserID = converter.StringToInt64(answerUserID)
addActivity.OriginalObjectID = questionObjID // if activity is 'accept' means this question is accept the answer.
} else {
addActivity.UserID = answerUserID
addActivity.TriggerUserID = converter.StringToInt64(answerUserID)
addActivity.OriginalObjectID = answerObjID // if activity is 'accepted' means this answer was accepted.
}
if isSelf {
addActivity.Rank = 0
@ -222,7 +226,7 @@ func (ar *AnswerActivityRepo) AcceptAnswer(ctx context.Context,
// CancelAcceptAnswer accept other answer
func (ar *AnswerActivityRepo) CancelAcceptAnswer(ctx context.Context,
answerObjID, questionUserID, answerUserID string,
answerObjID, questionObjID, questionUserID, answerUserID string,
) (err error) {
addActivityList := make([]*entity.Activity, 0)
for _, action := range acceptActionList {
@ -239,8 +243,10 @@ func (ar *AnswerActivityRepo) CancelAcceptAnswer(ctx context.Context,
}
if action == acceptAction {
addActivity.UserID = questionUserID
addActivity.OriginalObjectID = questionObjID
} else {
addActivity.UserID = answerUserID
addActivity.OriginalObjectID = answerObjID
}
addActivityList = append(addActivityList, addActivity)
}
@ -265,8 +271,8 @@ func (ar *AnswerActivityRepo) CancelAcceptAnswer(ctx context.Context,
return nil, errors.InternalServer(reason.DatabaseError).WithError(e).WithStack()
}
if _, e := session.Where("id = ?", existsActivity.ID).Cols("`cancelled`").
Update(&entity.Activity{Cancelled: entity.ActivityCancelled}); e != nil {
if _, e := session.Where("id = ?", existsActivity.ID).Cols("cancelled", "cancelled_at").
Update(&entity.Activity{Cancelled: entity.ActivityCancelled, CancelledAt: time.Now()}); e != nil {
return nil, errors.InternalServer(reason.DatabaseError).WithError(e).WithStack()
}
}
@ -326,8 +332,8 @@ func (ar *AnswerActivityRepo) DeleteAnswer(ctx context.Context, answerID string)
return nil, errors.InternalServer(reason.DatabaseError).WithError(e).WithStack()
}
if _, e := session.Where("id = ?", act.ID).Cols("`cancelled`").
Update(&entity.Activity{Cancelled: entity.ActivityCancelled}); e != nil {
if _, e := session.Where("id = ?", act.ID).Cols("cancelled", "cancelled_at").
Update(&entity.Activity{Cancelled: entity.ActivityCancelled, CancelledAt: time.Now()}); e != nil {
return nil, errors.InternalServer(reason.DatabaseError).WithError(e).WithStack()
}
}

View File

@ -2,6 +2,7 @@ package activity
import (
"context"
"time"
"github.com/answerdev/answer/internal/service/activity_common"
"github.com/answerdev/answer/internal/service/follow"
@ -59,7 +60,7 @@ func (ar *FollowRepo) Follow(ctx context.Context, objectID, userID string) error
return
}
if has && existsActivity.Cancelled == 0 {
if has && existsActivity.Cancelled == entity.ActivityAvailable {
return
}
@ -67,17 +68,18 @@ func (ar *FollowRepo) Follow(ctx context.Context, objectID, userID string) error
_, err = session.Where(builder.Eq{"id": existsActivity.ID}).
Cols(`cancelled`).
Update(&entity.Activity{
Cancelled: 0,
Cancelled: entity.ActivityAvailable,
})
} else {
// update existing activity with new user id and u object id
_, err = session.Insert(&entity.Activity{
UserID: userID,
ObjectID: objectID,
ActivityType: activityType,
Cancelled: 0,
Rank: 0,
HasRank: 0,
UserID: userID,
ObjectID: objectID,
OriginalObjectID: objectID,
ActivityType: activityType,
Cancelled: entity.ActivityAvailable,
Rank: 0,
HasRank: 0,
})
}
@ -120,13 +122,14 @@ func (ar *FollowRepo) FollowCancel(ctx context.Context, objectID, userID string)
return
}
if has && existsActivity.Cancelled == 1 {
if has && existsActivity.Cancelled == entity.ActivityCancelled {
return
}
if _, err = session.Where("id = ?", existsActivity.ID).
Cols("cancelled").
Update(&entity.Activity{
Cancelled: 1,
Cancelled: entity.ActivityCancelled,
CancelledAt: time.Now(),
}); err != nil {
return
}

View File

@ -55,11 +55,12 @@ func (ar *UserActiveActivityRepo) UserActive(ctx context.Context, userID string)
}
addActivity := &entity.Activity{
UserID: userID,
ObjectID: "0",
ActivityType: activityType,
Rank: deltaRank,
HasRank: 1,
UserID: userID,
ObjectID: "0",
OriginalObjectID: "0",
ActivityType: activityType,
Rank: deltaRank,
HasRank: 1,
}
_, exists, err := ar.activityRepo.GetActivity(ctx, session, "0", addActivity.UserID, activityType)
if err != nil {

View File

@ -3,6 +3,7 @@ package activity
import (
"context"
"strings"
"time"
"github.com/answerdev/answer/pkg/converter"
@ -100,18 +101,19 @@ func (vr *VoteRepo) vote(ctx context.Context, objectID string, userID, objectUse
Get(&existsActivity)
// is is voted,return
if has && existsActivity.Cancelled == 0 {
if has && existsActivity.Cancelled == entity.ActivityAvailable {
return
}
insertActivity = entity.Activity{
ObjectID: objectID,
UserID: activityUserID,
TriggerUserID: converter.StringToInt64(triggerUserID),
ActivityType: activityType,
Rank: deltaRank,
HasRank: hasRank,
Cancelled: 0,
ObjectID: objectID,
OriginalObjectID: objectID,
UserID: activityUserID,
TriggerUserID: converter.StringToInt64(triggerUserID),
ActivityType: activityType,
Rank: deltaRank,
HasRank: hasRank,
Cancelled: entity.ActivityAvailable,
}
// trigger user rank and send notification
@ -131,7 +133,7 @@ func (vr *VoteRepo) vote(ctx context.Context, objectID string, userID, objectUse
if has {
if _, err = session.Where("id = ?", existsActivity.ID).Cols("`cancelled`").
Update(&entity.Activity{
Cancelled: 0,
Cancelled: entity.ActivityAvailable,
}); err != nil {
return
}
@ -201,13 +203,14 @@ func (vr *VoteRepo) voteCancel(ctx context.Context, objectID string, userID, obj
return
}
if existsActivity.Cancelled == 1 {
if existsActivity.Cancelled == entity.ActivityCancelled {
return
}
if _, err = session.Where("id = ?", existsActivity.ID).Cols("`cancelled`").
if _, err = session.Where("id = ?", existsActivity.ID).Cols("cancelled", "cancelled_at").
Update(&entity.Activity{
Cancelled: 1,
Cancelled: entity.ActivityCancelled,
CancelledAt: time.Now(),
}); err != nil {
return
}

View File

@ -63,6 +63,14 @@ func (ar *ActivityRepo) GetActivityTypeByObjKey(ctx context.Context, objectKey,
return
}
func (ar *ActivityRepo) GetActivityTypeByConfigKey(ctx context.Context, configKey string) (activityType int, err error) {
activityType, err = ar.configRepo.GetConfigType(configKey)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
func (ar *ActivityRepo) GetActivity(ctx context.Context, session *xorm.Session,
objectID, userID string, activityType int,
) (existsActivity *entity.Activity, exist bool, err error) {
@ -89,3 +97,12 @@ func (ar *ActivityRepo) GetUserIDObjectIDActivitySum(ctx context.Context, userID
}
return sum.Rank, nil
}
// AddActivity add activity
func (ar *ActivityRepo) AddActivity(ctx context.Context, activity *entity.Activity) (err error) {
_, err = ar.data.DB.Insert(activity)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}

View File

@ -124,7 +124,7 @@ func (ar *FollowRepo) IsFollowed(userID, objectID string) (bool, error) {
if !has {
return false, nil
}
if at.Cancelled == 1 {
if at.Cancelled == entity.ActivityCancelled {
return false, nil
} else {
return true, nil

View File

@ -53,6 +53,7 @@ var ProviderSetRepo = wire.NewSet(
activity.NewAnswerActivityRepo,
activity.NewQuestionActivityRepo,
activity.NewUserActiveActivityRepo,
activity.NewActivityRepo,
tag.NewTagRepo,
tag_common.NewTagCommonRepo,
tag.NewTagRelRepo,

View File

@ -166,7 +166,7 @@ func (qr *questionRepo) GetQuestionList(ctx context.Context, question *entity.Qu
func (qr *questionRepo) GetQuestionCount(ctx context.Context) (count int64, err error) {
questionList := make([]*entity.Question, 0)
count, err = qr.data.DB.In("question.status", []int{entity.QuestionStatusAvailable, entity.QuestionStatusclosed}).FindAndCount(&questionList)
count, err = qr.data.DB.In("question.status", []int{entity.QuestionStatusAvailable, entity.QuestionStatusClosed}).FindAndCount(&questionList)
if err != nil {
return count, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
@ -210,7 +210,7 @@ func (qr *questionRepo) SearchList(ctx context.Context, search *schema.QuestionS
session = session.And("question.user_id = ?", search.UserID)
}
session = session.In("question.status", []int{entity.QuestionStatusAvailable, entity.QuestionStatusclosed})
session = session.In("question.status", []int{entity.QuestionStatusAvailable, entity.QuestionStatusClosed})
// if search.Status > 0 {
// session = session.And("question.status = ?", search.Status)
// }
@ -230,7 +230,7 @@ func (qr *questionRepo) SearchList(ctx context.Context, search *schema.QuestionS
session = session.OrderBy("question.created_at desc")
}
session = session.Limit(search.PageSize, offset)
session = session.Select("question.id,question.user_id,question.title,question.original_text,question.parsed_text,question.status,question.view_count,question.unique_view_count,question.vote_count,question.answer_count,question.collection_count,question.follow_count,question.accepted_answer_id,question.last_answer_id,question.created_at,question.updated_at,question.post_update_time,question.revision_id")
session = session.Select("question.id,question.user_id,last_edit_user_id,question.title,question.original_text,question.parsed_text,question.status,question.view_count,question.unique_view_count,question.vote_count,question.answer_count,question.collection_count,question.follow_count,question.accepted_answer_id,question.last_answer_id,question.created_at,question.updated_at,question.post_update_time,question.revision_id")
count, err = session.FindAndCount(&rows)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()

View File

@ -54,7 +54,7 @@ func Test_tagRepo_GetTagByID(t *testing.T) {
tagOnce.Do(addTagList)
tagCommonRepo := tag_common.NewTagCommonRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource))
gotTag, exist, err := tagCommonRepo.GetTagByID(context.TODO(), testTagList[0].ID)
gotTag, exist, err := tagCommonRepo.GetTagByID(context.TODO(), testTagList[0].ID, true)
assert.NoError(t, err)
assert.True(t, exist)
assert.Equal(t, testTagList[0].SlugName, gotTag.SlugName)
@ -139,7 +139,7 @@ func Test_tagRepo_UpdateTag(t *testing.T) {
tagCommonRepo := tag_common.NewTagCommonRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource))
gotTag, exist, err := tagCommonRepo.GetTagByID(context.TODO(), testTagList[0].ID)
gotTag, exist, err := tagCommonRepo.GetTagByID(context.TODO(), testTagList[0].ID, true)
assert.NoError(t, err)
assert.True(t, exist)
assert.Equal(t, testTagList[0].DisplayName, gotTag.DisplayName)
@ -152,7 +152,7 @@ func Test_tagRepo_UpdateTagQuestionCount(t *testing.T) {
err := tagCommonRepo.UpdateTagQuestionCount(context.TODO(), testTagList[0].ID, 100)
assert.NoError(t, err)
gotTag, exist, err := tagCommonRepo.GetTagByID(context.TODO(), testTagList[0].ID)
gotTag, exist, err := tagCommonRepo.GetTagByID(context.TODO(), testTagList[0].ID, true)
assert.NoError(t, err)
assert.True(t, exist)
assert.Equal(t, 100, gotTag.QuestionCount)
@ -172,7 +172,7 @@ func Test_tagRepo_UpdateTagSynonym(t *testing.T) {
tagCommonRepo := tag_common.NewTagCommonRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource))
gotTag, exist, err := tagCommonRepo.GetTagByID(context.TODO(), testTagList[2].ID)
gotTag, exist, err := tagCommonRepo.GetTagByID(context.TODO(), testTagList[2].ID, true)
assert.NoError(t, err)
assert.True(t, exist)
assert.Equal(t, testTagList[0].ID, fmt.Sprintf("%d", gotTag.MainTagID))

View File

@ -5,10 +5,12 @@ import (
"github.com/answerdev/answer/internal/base/constant"
"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"
"github.com/answerdev/answer/internal/service/revision"
"github.com/answerdev/answer/internal/service/unique"
"github.com/answerdev/answer/pkg/converter"
"github.com/answerdev/answer/pkg/obj"
"github.com/segmentfault/pacman/errors"
"xorm.io/builder"
@ -79,6 +81,22 @@ func (rr *revisionRepo) UpdateObjectRevisionId(ctx context.Context, revision *en
return nil
}
// UpdateStatus update revision status
func (rr *revisionRepo) UpdateStatus(ctx context.Context, id string, status int, reviewUserID string) (err error) {
if id == "" {
return nil
}
var data entity.Revision
data.ID = id
data.Status = status
data.ReviewUserID = converter.StringToInt64(reviewUserID)
_, err = rr.data.DB.Where("id =?", id).Cols("status", "review_user_id").Update(&data)
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return nil
}
// GetRevision get revision one
func (rr *revisionRepo) GetRevision(ctx context.Context, id string) (
revision *entity.Revision, exist bool, err error,
@ -91,6 +109,27 @@ func (rr *revisionRepo) GetRevision(ctx context.Context, id string) (
return
}
// GetRevisionByID get object's last revision by object TagID
func (rr *revisionRepo) GetRevisionByID(ctx context.Context, revisionID string) (
revision *entity.Revision, exist bool, err error) {
revision = &entity.Revision{}
exist, err = rr.data.DB.Where("id = ?", revisionID).Get(revision)
if err != nil {
return nil, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
func (rr *revisionRepo) ExistUnreviewedByObjectID(ctx context.Context, objectID string) (
revision *entity.Revision, exist bool, err error) {
revision = &entity.Revision{}
exist, err = rr.data.DB.Where("object_id = ?", objectID).And("status = ?", entity.RevisionUnreviewedStatus).Get(revision)
if err != nil {
return nil, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// GetLastRevisionByObjectID get object's last revision by object TagID
func (rr *revisionRepo) GetLastRevisionByObjectID(ctx context.Context, objectID string) (
revision *entity.Revision, exist bool, err error,
@ -128,3 +167,22 @@ func (rr *revisionRepo) allowRecord(objectType int) (ok bool) {
return false
}
}
// GetUnreviewedRevisionPage get unreviewed revision page
func (rr *revisionRepo) GetUnreviewedRevisionPage(ctx context.Context, page int, pageSize int,
objectTypeList []int) (revisionList []*entity.Revision, total int64, err error) {
revisionList = make([]*entity.Revision, 0)
if len(objectTypeList) == 0 {
return revisionList, 0, nil
}
session := rr.data.DB.NewSession()
session = session.And("status = ?", entity.RevisionUnreviewedStatus)
session = session.In("object_type", objectTypeList)
session = session.OrderBy("created_at asc")
total, err = pager.Help(page, pageSize, &revisionList, &entity.Revision{}, session)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}

View File

@ -155,13 +155,14 @@ func (sr *searchRepo) SearchContents(ctx context.Context, words []string, tagIDs
if err != nil {
return
}
sql := fmt.Sprintf("(%s UNION ALL %s)", ubSQL, bSQL)
sql := fmt.Sprintf("(%s UNION ALL %s)", bSQL, ubSQL)
querySQL, _, err := builder.MySQL().Select("*").From(sql, "t").OrderBy(sr.parseOrder(ctx, order)).Limit(size, page-1).ToSQL()
countSQL, _, err := builder.MySQL().Select("count(*) total").From(sql, "c").ToSQL()
if err != nil {
return
}
countSQL, _, err := builder.MySQL().Select("count(*) total").From(sql, "c").ToSQL()
querySQL, _, err := builder.MySQL().Select("*").From(sql, "t").OrderBy(sr.parseOrder(ctx, order)).Limit(size, page-1).ToSQL()
if err != nil {
return
}
@ -197,15 +198,17 @@ func (sr *searchRepo) SearchContents(ctx context.Context, words []string, tagIDs
// SearchQuestions search question data
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
}
words = filterWords(words)
var (
qfs = qFields
args = []interface{}{}
)
if order == "relevance" {
qfs, args = addRelevanceField([]string{"title", "original_text"}, words, qfs)
if len(words) > 0 {
qfs, args = addRelevanceField([]string{"title", "original_text"}, words, qfs)
} else {
order = "newest"
}
}
b := builder.MySQL().Select(qfs...).From("question")
@ -257,11 +260,12 @@ func (sr *searchRepo) SearchQuestions(ctx context.Context, words []string, notAc
queryArgs := []interface{}{}
countArgs := []interface{}{}
querySQL, _, err := b.OrderBy(sr.parseOrder(ctx, order)).Limit(size, page-1).ToSQL()
countSQL, _, err := builder.MySQL().Select("count(*) total").From(b, "c").ToSQL()
if err != nil {
return
}
countSQL, _, err := builder.MySQL().Select("count(*) total").From(b, "c").ToSQL()
querySQL, _, err := b.OrderBy(sr.parseOrder(ctx, order)).Limit(size, page-1).ToSQL()
if err != nil {
return
}
@ -293,15 +297,18 @@ func (sr *searchRepo) SearchQuestions(ctx context.Context, words []string, notAc
// SearchAnswers search answer data
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
}
words = filterWords(words)
var (
afs = aFields
args = []interface{}{}
)
if order == "relevance" {
afs, args = addRelevanceField([]string{"`answer`.`original_text`"}, words, afs)
if len(words) > 0 {
afs, args = addRelevanceField([]string{"`answer`.`original_text`"}, words, afs)
} else {
order = "newest"
}
}
b := builder.MySQL().Select(afs...).From("`answer`").
@ -346,14 +353,16 @@ func (sr *searchRepo) SearchAnswers(ctx context.Context, words []string, tagIDs
queryArgs := []interface{}{}
countArgs := []interface{}{}
querySQL, _, err := b.OrderBy(sr.parseOrder(ctx, order)).Limit(size, page-1).ToSQL()
if err != nil {
return
}
countSQL, _, err := builder.MySQL().Select("count(*) total").From(b, "c").ToSQL()
if err != nil {
return
}
querySQL, _, err := b.OrderBy(sr.parseOrder(ctx, order)).Limit(size, page-1).ToSQL()
if err != nil {
return
}
queryArgs = append(queryArgs, querySQL)
queryArgs = append(queryArgs, args...)
@ -471,20 +480,6 @@ func (sr *searchRepo) parseResult(ctx context.Context, res []map[string][]byte)
return
}
// userBasicInfoFormat
func (sr *searchRepo) userBasicInfoFormat(ctx context.Context, dbinfo *entity.User) *schema.UserBasicInfo {
return &schema.UserBasicInfo{
ID: dbinfo.ID,
Username: dbinfo.Username,
Rank: dbinfo.Rank,
DisplayName: dbinfo.DisplayName,
Avatar: dbinfo.Avatar,
Website: dbinfo.Website,
Location: dbinfo.Location,
IPInfo: dbinfo.IPInfo,
}
}
func addRelevanceField(searchFields, words, fields []string) (res []string, args []interface{}) {
relevanceRes := []string{}
args = []interface{}{}

View File

@ -6,7 +6,7 @@ import (
"github.com/answerdev/answer/internal/base/data"
"github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/internal/entity"
"github.com/answerdev/answer/internal/service/tag"
"github.com/answerdev/answer/internal/service/tag_common"
"github.com/answerdev/answer/internal/service/unique"
"github.com/segmentfault/pacman/errors"
"xorm.io/builder"
@ -22,7 +22,7 @@ type tagRepo struct {
func NewTagRepo(
data *data.Data,
uniqueIDRepo unique.UniqueIDRepo,
) tag.TagRepo {
) tag_common.TagRepo {
return &tagRepo{
data: data,
uniqueIDRepo: uniqueIDRepo,

View File

@ -122,12 +122,14 @@ func (tr *tagCommonRepo) GetTagListByNames(ctx context.Context, names []string)
}
// GetTagByID get tag one
func (tr *tagCommonRepo) GetTagByID(ctx context.Context, tagID string) (
func (tr *tagCommonRepo) GetTagByID(ctx context.Context, tagID string, includeDeleted bool) (
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})
if !includeDeleted {
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()

View File

@ -28,7 +28,7 @@ func NewUserRepo(data *data.Data, configRepo config.ConfigRepo) usercommon.UserR
// AddUser add user
func (ur *userRepo) AddUser(ctx context.Context, user *entity.User) (err error) {
_, err = ur.data.DB.Insert(user)
_, err = ur.data.DB.UseBool("is_admin").Insert(user)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}

View File

@ -29,6 +29,7 @@ type AnswerAPIRouter struct {
notificationController *controller.NotificationController
dashboardController *controller.DashboardController
uploadController *controller.UploadController
activityController *controller.ActivityController
}
func NewAnswerAPIRouter(
@ -54,6 +55,7 @@ func NewAnswerAPIRouter(
notificationController *controller.NotificationController,
dashboardController *controller.DashboardController,
uploadController *controller.UploadController,
activityController *controller.ActivityController,
) *AnswerAPIRouter {
return &AnswerAPIRouter{
langController: langController,
@ -78,6 +80,7 @@ func NewAnswerAPIRouter(
siteinfoController: siteinfoController,
dashboardController: dashboardController,
uploadController: uploadController,
activityController: activityController,
}
}
@ -137,9 +140,15 @@ 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) {
//revisions
r.GET("/revisions/unreviewed", a.revisionController.GetUnreviewedRevisionList)
r.PUT("/revisions/audit", a.revisionController.RevisionAudit)
r.GET("/revisions/edit/check", a.revisionController.CheckCanUpdateRevision)
// comment
r.POST("/comment", a.commentController.AddComment)
r.DELETE("/comment", a.commentController.RemoveComment)
@ -200,6 +209,11 @@ func (a *AnswerAPIRouter) RegisterAnswerAPIRouter(r *gin.RouterGroup) {
// upload file
r.POST("/file", a.uploadController.UploadFile)
// activity
r.GET("/activity/timeline", a.activityController.GetObjectTimeline)
r.GET("/activity/timeline/detail", a.activityController.GetObjectTimelineDetail)
}
func (a *AnswerAPIRouter) RegisterAnswerCmsAPIRouter(r *gin.RouterGroup) {

View File

@ -0,0 +1,85 @@
package schema
import "github.com/answerdev/answer/internal/base/constant"
// ActivityMsg activity message
type ActivityMsg struct {
UserID string `json:"user_id"`
TriggerUserID int64 `json:"trigger_user_id"`
ObjectID string `json:"object_id"`
OriginalObjectID string `json:"original_object_id"`
ActivityTypeKey constant.ActivityTypeKey `json:"activity_type_key"`
RevisionID string `json:"revision_id"`
}
// GetObjectTimelineReq get object timeline request
type GetObjectTimelineReq struct {
ObjectID string `validate:"omitempty,gt=0,lte=100" form:"object_id"`
ShowVote bool `validate:"omitempty" form:"show_vote"`
UserID string `json:"-"`
IsAdmin bool `json:"-"`
}
// GetObjectTimelineResp get object timeline response
type GetObjectTimelineResp struct {
ObjectInfo *ActObjectInfo `json:"object_info"`
Timeline []*ActObjectTimeline `json:"timeline"`
}
// ActObjectTimeline act object timeline
type ActObjectTimeline struct {
ActivityID string `json:"activity_id"`
RevisionID string `json:"revision_id"`
CreatedAt int64 `json:"created_at"`
ActivityType string `json:"activity_type"`
Username string `json:"username"`
UserDisplayName string `json:"user_display_name"`
Comment string `json:"comment"`
ObjectID string `json:"object_id"`
ObjectType string `json:"object_type"`
Cancelled bool `json:"cancelled"`
CancelledAt int64 `json:"cancelled_at"`
UserID string `json:"-"`
}
// ActObjectInfo act object info
type ActObjectInfo struct {
ObjectType string `json:"object_type"`
Title string `json:"title"`
QuestionID string `json:"question_id"`
AnswerID string `json:"answer_id"`
MainTagSlugName string `json:"main_tag_slug_name"`
Username string `json:"username"`
DisplayName string `json:"display_name"`
}
// GetObjectTimelineDetailReq get object timeline detail request
type GetObjectTimelineDetailReq struct {
NewRevisionID string `validate:"required,gt=0,lte=100" form:"new_revision_id"`
OldRevisionID string `validate:"required,gt=0,lte=100" form:"old_revision_id"`
UserID string `json:"-"`
}
// GetObjectTimelineDetailResp get object timeline detail response
type GetObjectTimelineDetailResp struct {
NewRevision *ObjectTimelineDetail `json:"new_revision"`
OldRevision *ObjectTimelineDetail `json:"old_revision"`
}
// ObjectTimelineDetail object timeline detail
type ObjectTimelineDetail struct {
Title string `json:"title"`
Tags []*ObjectTimelineTag `json:"tags"`
OriginalText string `json:"original_text"`
SlugName string `json:"slug_name"`
MainTagSlugName string `json:"main_tag_slug_name"`
}
// ObjectTimelineTag object timeline tags
type ObjectTimelineTag struct {
SlugName string `json:"slug_name"`
DisplayName string `json:"display_name"`
MainTagSlugName string `json:"main_tag_slug_name"`
Recommend bool `json:"recommend"`
Reserved bool `json:"reserved"`
}

View File

@ -5,7 +5,8 @@ type RemoveAnswerReq struct {
// answer id
ID string `validate:"required" json:"id"`
// user id
UserID string `json:"-"`
UserID string `json:"-"`
IsAdmin bool `json:"-"`
}
const (
@ -21,21 +22,34 @@ type AnswerAddReq struct {
}
type AnswerUpdateReq struct {
ID string `json:"id"` // id
QuestionID string `json:"question_id" ` // question_id
UserID string `json:"-" ` // user_id
Title string `json:"title" ` // title
Content string `json:"content"` // content
HTML string `json:"html" ` // html
EditSummary string `validate:"omitempty" json:"edit_summary"` // edit_summary
ID string `json:"id"` // id
QuestionID string `json:"question_id" ` // question_id
UserID string `json:"-" ` // user_id
Title string `json:"title" ` // title
Content string `json:"content"` // content
HTML string `json:"html" ` // html
EditSummary string `validate:"omitempty" json:"edit_summary"` // edit_summary
NoNeedReview bool `json:"-"`
// whether user can edit it
CanEdit bool `json:"-"`
}
type AnswerList struct {
QuestionID string `json:"question_id" form:"question_id"` // question_id
Order string `json:"order" form:"order"` // 1 Default 2 time
Page int `json:"page" form:"page"` // Query number of pages
PageSize int `json:"page_size" form:"page_size"` // Search page size
LoginUserID string `json:"-" `
// AnswerUpdateResp answer update resp
type AnswerUpdateResp struct {
WaitForReview bool `json:"wait_for_review"`
}
type AnswerListReq struct {
QuestionID string `json:"question_id" form:"question_id"` // question_id
Order string `json:"order" form:"order"` // 1 Default 2 time
Page int `json:"page" form:"page"` // Query number of pages
PageSize int `json:"page_size" form:"page_size"` // Search page size
UserID string `json:"-" `
IsAdmin bool `json:"-"`
// whether user can edit it
CanEdit bool `json:"-"`
// whether user can delete it
CanDelete bool `json:"-"`
}
type AnswerInfo struct {
@ -47,6 +61,7 @@ type AnswerInfo struct {
UpdateTime int64 `json:"update_time" xorm:"updated"` // update_time
Adopted int `json:"adopted"` // 1 Failed 2 Adopted
UserID string `json:"-" `
UpdateUserID string `json:"-" `
UserInfo *UserBasicInfo `json:"user_info,omitempty"`
UpdateUserInfo *UserBasicInfo `json:"update_user_info,omitempty"`
Collected bool `json:"collected"`
@ -66,6 +81,7 @@ type AdminAnswerInfo struct {
UpdateTime int64 `json:"update_time"`
Adopted int `json:"adopted"`
UserID string `json:"-" `
UpdateUserID string `json:"-" `
UserInfo *UserBasicInfo `json:"user_info"`
VoteCount int `json:"vote_count"`
QuestionInfo struct {
@ -74,7 +90,13 @@ type AdminAnswerInfo struct {
}
type AnswerAdoptedReq struct {
QuestionID string `json:"question_id" ` // question_id
AnswerID string `json:"answer_id" `
QuestionID string `json:"question_id"`
AnswerID string `json:"answer_id"`
UserID string `json:"-" `
}
type AdminSetAnswerStatusRequest struct {
StatusStr string `json:"status"`
AnswerID string `json:"answer_id"`
UserID string `json:"-" `
}

View File

@ -19,6 +19,12 @@ type AddCommentReq struct {
MentionUsernameList []string `validate:"omitempty" json:"mention_username_list"`
// user id
UserID string `json:"-"`
// whether user can add it
CanAdd bool `json:"-"`
// whether user can edit it
CanEdit bool `json:"-"`
// whether user can delete it
CanDelete bool `json:"-"`
}
// RemoveCommentReq remove comment
@ -69,10 +75,16 @@ type GetCommentWithPageReq struct {
PageSize int `validate:"omitempty,min=1" form:"page_size"`
// object id
ObjectID string `validate:"required" form:"object_id"`
// comment id
CommentID string `validate:"omitempty" form:"comment_id"`
// query condition
QueryCond string `validate:"omitempty,oneof=vote" form:"query_cond"`
// user id
UserID string `json:"-"`
// whether user can edit it
CanEdit bool `json:"-"`
// whether user can delete it
CanDelete bool `json:"-"`
}
// GetCommentReq get comment list page request
@ -81,6 +93,10 @@ type GetCommentReq struct {
ID string `validate:"required" form:"id"`
// user id
UserID string `json:"-"`
// whether user can edit it
CanEdit bool `json:"-"`
// whether user can delete it
CanDelete bool `json:"-"`
}
// GetCommentResp comment response

View File

@ -27,6 +27,13 @@ type NotificationContent struct {
UpdateTime int64 `json:"update_time"`
}
type GetRedDot struct {
CanReviewQuestion bool `json:"-"`
CanReviewAnswer bool `json:"-"`
CanReviewTag bool `json:"-"`
UserID string `json:"-"`
}
// NotificationMsg notification message
type NotificationMsg struct {
// trigger notification user id
@ -57,6 +64,8 @@ type ObjectInfo struct {
type RedDot struct {
Inbox int64 `json:"inbox"`
Achievement int64 `json:"achievement"`
Revision int64 `json:"revision"`
CanRevision bool `json:"can_revision"`
}
type NotificationSearch struct {
@ -68,8 +77,11 @@ type NotificationSearch struct {
}
type NotificationClearRequest struct {
UserID string `json:"-"`
TypeStr string `json:"type" form:"type"` // inbox achievement
UserID string `json:"-"`
TypeStr string `json:"type" form:"type"` // inbox achievement
CanReviewQuestion bool `json:"-"`
CanReviewAnswer bool `json:"-"`
CanReviewTag bool `json:"-"`
}
type NotificationClearIDRequest struct {

View File

@ -3,8 +3,9 @@ package schema
// RemoveQuestionReq delete question request
type RemoveQuestionReq struct {
// question id
ID string `validate:"required" comment:"question id" json:"id"`
UserID string `json:"-" ` // user_id
ID string `validate:"required" comment:"question id" json:"id"`
UserID string `json:"-" ` // user_id
IsAdmin bool `json:"-"`
}
type CloseQuestionReq struct {
@ -12,6 +13,7 @@ type CloseQuestionReq struct {
UserID string `json:"-" ` // user_id
CloseType int `json:"close_type" ` // close_type
CloseMsg string `json:"close_msg" ` // close_type
IsAdmin bool `json:"-"`
}
type CloseQuestionMeta struct {
@ -30,6 +32,26 @@ type QuestionAdd struct {
Tags []*TagItem `validate:"required,dive" json:"tags"`
// user id
UserID string `json:"-"`
QuestionPermission
}
type QuestionPermission struct {
// whether user can add it
CanAdd bool `json:"-"`
// whether user can edit it
CanEdit bool `json:"-"`
// whether user can delete it
CanDelete bool `json:"-"`
// whether user can close it
CanClose bool `json:"-"`
}
type CheckCanQuestionUpdate struct {
// question id
ID string `validate:"required" form:"id"`
// user id
UserID string `json:"-"`
IsAdmin bool `json:"-"`
}
type QuestionUpdate struct {
@ -46,7 +68,10 @@ type QuestionUpdate struct {
// edit summary
EditSummary string `validate:"omitempty" json:"edit_summary"`
// user id
UserID string `json:"-"`
UserID string `json:"-"`
IsAdmin bool `json:"-"`
NoNeedReview bool `json:"-"`
QuestionPermission
}
type QuestionBaseInfo struct {
@ -81,6 +106,8 @@ type QuestionInfo struct {
Status int `json:"status"`
Operation *Operation `json:"operation,omitempty"`
UserID string `json:"-" `
LastEditUserID string `json:"-" `
LastAnsweredUserID string `json:"-" `
UserInfo *UserBasicInfo `json:"user_info"`
UpdateUserInfo *UserBasicInfo `json:"update_user_info,omitempty"`
LastAnsweredUserInfo *UserBasicInfo `json:"last_answered_user_info,omitempty"`
@ -93,6 +120,11 @@ type QuestionInfo struct {
MemberActions []*PermissionMemberAction `json:"member_actions"`
}
// UpdateQuestionResp update question resp
type UpdateQuestionResp struct {
WaitForReview bool `json:"wait_for_review"`
}
type AdminQuestionInfo struct {
ID string `json:"id"`
Title string `json:"title"`
@ -169,7 +201,7 @@ type CmsQuestionSearch struct {
Page int `json:"page" form:"page"` // Query number of pages
PageSize int `json:"page_size" form:"page_size"` // Search page size
Status int `json:"-" form:"-"`
StatusStr string `json:"status" form:"status"` // Status 1 Available 2 closed 10 UserDeleted
StatusStr string `json:"status" form:"status"` // Status 1 Available 2 closed 10 UserDeleted
Query string `validate:"omitempty,gt=0,lte=100" json:"query" form:"query" ` //Query string
}

View File

@ -2,6 +2,8 @@ package schema
import (
"time"
"github.com/answerdev/answer/internal/base/constant"
)
// AddRevisionDTO add revision request
@ -16,6 +18,8 @@ type AddRevisionDTO struct {
Content string
// log
Log string
// status
Status int
}
// GetRevisionListReq get revision list all request
@ -24,6 +28,47 @@ type GetRevisionListReq struct {
ObjectID string `validate:"required" comment:"object_id" form:"object_id"`
}
const RevisionAuditApprove = "approve"
const RevisionAuditReject = "reject"
type RevisionAuditReq struct {
// object id
ID string `validate:"required" comment:"id" form:"id"`
Operation string `validate:"required" comment:"operation" form:"operation"` //approve or reject
UserID string `json:"-"`
CanReviewQuestion bool `json:"-"`
CanReviewAnswer bool `json:"-"`
CanReviewTag bool `json:"-"`
}
type RevisionSearch struct {
Page int `json:"page" form:"page"` // Query number of pages
CanReviewQuestion bool `json:"-"`
CanReviewAnswer bool `json:"-"`
CanReviewTag bool `json:"-"`
UserID string `json:"-"`
}
func (r RevisionSearch) GetCanReviewObjectTypes() []int {
objectType := make([]int, 0)
if r.CanReviewAnswer {
objectType = append(objectType, constant.ObjectTypeStrMapping[constant.AnswerObjectType])
}
if r.CanReviewQuestion {
objectType = append(objectType, constant.ObjectTypeStrMapping[constant.QuestionObjectType])
}
if r.CanReviewTag {
objectType = append(objectType, constant.ObjectTypeStrMapping[constant.TagObjectType])
}
return objectType
}
type GetUnreviewedRevisionResp struct {
Type string `json:"type"`
Info *UnreviewedRevisionInfoInfo `json:"info"`
UnreviewedInfo *GetRevisionResp `json:"unreviewed_info"`
}
// GetRevisionResp get revision response
type GetRevisionResp struct {
// id

View File

@ -2,13 +2,21 @@ package schema
// SimpleObjectInfo simple object info
type SimpleObjectInfo struct {
ObjectID string `json:"object_id"`
ObjectCreator string `json:"object_creator"`
QuestionID string `json:"question_id"`
AnswerID string `json:"answer_id"`
CommentID string `json:"comment_id"`
TagID string `json:"tag_id"`
ObjectType string `json:"object_type"`
Title string `json:"title"`
Content string `json:"content"`
ObjectID string `json:"object_id"`
ObjectCreatorUserID string `json:"object_creator_user_id"`
QuestionID string `json:"question_id"`
AnswerID string `json:"answer_id"`
CommentID string `json:"comment_id"`
TagID string `json:"tag_id"`
ObjectType string `json:"object_type"`
Title string `json:"title"`
Content string `json:"content"`
}
type UnreviewedRevisionInfoInfo struct {
ObjectID string `json:"object_id"`
Title string `json:"title"`
Content string `json:"content"`
Html string `json:"html"`
Tags []*TagResp `json:"tags"`
}

View File

@ -8,8 +8,8 @@ import (
// SiteGeneralReq site general request
type SiteGeneralReq struct {
Name string `validate:"required,gt=1,lte=128" form:"name" json:"name"`
ShortDescription string `validate:"required,gt=3,lte=255" form:"short_description" json:"short_description"`
Description string `validate:"required,gt=3,lte=2000" form:"description" json:"description"`
ShortDescription string `validate:"omitempty,gt=3,lte=255" form:"short_description" json:"short_description"`
Description string `validate:"omitempty,gt=3,lte=2000" form:"description" json:"description"`
SiteUrl string `validate:"required,gt=1,lte=512,url" form:"site_url" json:"site_url"`
ContactEmail string `validate:"required,gt=1,lte=512,email" form:"contact_email" json:"contact_email"`
}

View File

@ -23,6 +23,10 @@ type GetTagInfoReq struct {
Name string `validate:"omitempty,gt=0,lte=35" form:"name"`
// user id
UserID string `json:"-"`
// whether user can edit it
CanEdit bool `json:"-"`
// whether user can delete it
CanDelete bool `json:"-"`
}
func (r *GetTagInfoReq) Check() (errFields []*validator.FormErrorField, err error) {
@ -152,7 +156,8 @@ type UpdateTagReq struct {
// edit summary
EditSummary string `validate:"omitempty" json:"edit_summary"`
// user id
UserID string `json:"-"`
UserID string `json:"-"`
NoNeedReview bool `json:"-"`
}
func (r *UpdateTagReq) Check() (errFields []*validator.FormErrorField, err error) {
@ -162,6 +167,11 @@ func (r *UpdateTagReq) Check() (errFields []*validator.FormErrorField, err error
return nil, nil
}
// UpdateTagResp update tag response
type UpdateTagResp struct {
WaitForReview bool `json:"wait_for_review"`
}
// GetTagWithPageReq get tag list page request
type GetTagWithPageReq struct {
// page
@ -182,10 +192,21 @@ type GetTagWithPageReq struct {
type GetTagSynonymsReq struct {
// tag_id
TagID string `validate:"required" form:"tag_id"`
// user id
UserID string `json:"-"`
// whether user can edit it
CanEdit bool `json:"-"`
}
// GetTagSynonymsResp get tag synonyms response
type GetTagSynonymsResp struct {
// synonyms
Synonyms []*TagSynonym `json:"synonyms"`
// MemberActions
MemberActions []*PermissionMemberAction `json:"member_actions"`
}
type TagSynonym struct {
// tag id
TagID string `json:"tag_id"`
// slug name

View File

@ -368,7 +368,8 @@ type ActionRecordResp struct {
}
type UserBasicInfo struct {
ID string `json:"-" ` // user_id
ID string `json:"-"` // user_id
IsAdmin bool `json:"-"`
Username string `json:"username" ` // name
Rank int `json:"rank" ` // rank
DisplayName string `json:"display_name"` // display_name

View File

@ -3,6 +3,7 @@ package schema
type VoteReq struct {
ObjectID string `validate:"required" form:"object_id" json:"object_id"` // id
IsCancel bool `validate:"omitempty" form:"is_cancel" json:"is_cancel"` // is cancel
UserID string `json:"-"`
}
type VoteDTO struct {

View File

@ -0,0 +1,302 @@
package activity
import (
"context"
"encoding/json"
"strings"
"github.com/answerdev/answer/internal/base/constant"
"github.com/answerdev/answer/internal/entity"
"github.com/answerdev/answer/internal/repo/config"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/activity_common"
"github.com/answerdev/answer/internal/service/comment_common"
"github.com/answerdev/answer/internal/service/meta"
"github.com/answerdev/answer/internal/service/object_info"
"github.com/answerdev/answer/internal/service/revision_common"
"github.com/answerdev/answer/internal/service/tag_common"
usercommon "github.com/answerdev/answer/internal/service/user_common"
"github.com/answerdev/answer/pkg/converter"
"github.com/answerdev/answer/pkg/obj"
"github.com/segmentfault/pacman/log"
)
// ActivityRepo activity repository
type ActivityRepo interface {
GetObjectAllActivity(ctx context.Context, objectID string, showVote bool) (activityList []*entity.Activity, err error)
}
// ActivityService activity service
type ActivityService struct {
activityRepo ActivityRepo
userCommon *usercommon.UserCommon
activityCommonService *activity_common.ActivityCommon
tagCommonService *tag_common.TagCommonService
objectInfoService *object_info.ObjService
commentCommonService *comment_common.CommentCommonService
revisionService *revision_common.RevisionService
metaService *meta.MetaService
}
// NewActivityService new activity service
func NewActivityService(
activityRepo ActivityRepo,
userCommon *usercommon.UserCommon,
activityCommonService *activity_common.ActivityCommon,
tagCommonService *tag_common.TagCommonService,
objectInfoService *object_info.ObjService,
commentCommonService *comment_common.CommentCommonService,
revisionService *revision_common.RevisionService,
metaService *meta.MetaService,
) *ActivityService {
return &ActivityService{
objectInfoService: objectInfoService,
activityRepo: activityRepo,
userCommon: userCommon,
activityCommonService: activityCommonService,
tagCommonService: tagCommonService,
commentCommonService: commentCommonService,
revisionService: revisionService,
metaService: metaService,
}
}
// GetObjectTimeline get object timeline
func (as *ActivityService) GetObjectTimeline(ctx context.Context, req *schema.GetObjectTimelineReq) (
resp *schema.GetObjectTimelineResp, err error) {
resp = &schema.GetObjectTimelineResp{
ObjectInfo: &schema.ActObjectInfo{},
Timeline: make([]*schema.ActObjectTimeline, 0),
}
resp.ObjectInfo, err = as.getTimelineMainObjInfo(ctx, req.ObjectID)
if err != nil {
return nil, err
}
activityList, err := as.activityRepo.GetObjectAllActivity(ctx, req.ObjectID, req.ShowVote)
if err != nil {
return nil, err
}
for _, act := range activityList {
item := &schema.ActObjectTimeline{
ActivityID: act.ID,
RevisionID: converter.IntToString(act.RevisionID),
CreatedAt: act.CreatedAt.Unix(),
Cancelled: act.Cancelled == entity.ActivityCancelled,
ObjectID: act.ObjectID,
}
item.ObjectType, _ = obj.GetObjectTypeStrByObjectID(act.ObjectID)
if item.Cancelled {
item.CancelledAt = act.CancelledAt.Unix()
}
// database save activity type is number, change to activity type string is like "question.asked".
// so we need to cut the front part of '.'
_, item.ActivityType, _ = strings.Cut(config.ID2KeyMapping[act.ActivityType], ".")
// format activity type string to show
if isHidden, formattedActivityType := formatActivity(item.ActivityType); isHidden {
continue
} else {
item.ActivityType = formattedActivityType
}
// if activity is down vote, only admin can see who does it.
if item.ActivityType == constant.ActDownVote && !req.IsAdmin {
item.Username = "N/A"
item.UserDisplayName = "N/A"
} else {
item.UserID = act.UserID
}
item.Comment = as.getTimelineActivityComment(ctx, item.ObjectID, item.ObjectType, item.ActivityType, item.RevisionID)
resp.Timeline = append(resp.Timeline, item)
}
as.formatTimelineUserInfo(ctx, resp.Timeline)
return
}
func (as *ActivityService) getTimelineMainObjInfo(ctx context.Context, objectID string) (
resp *schema.ActObjectInfo, err error) {
resp = &schema.ActObjectInfo{}
objInfo, err := as.objectInfoService.GetInfo(ctx, objectID)
if err != nil {
return nil, err
}
resp.Title = objInfo.Title
if objInfo.ObjectType == constant.TagObjectType {
tag, exist, _ := as.tagCommonService.GetTagByID(ctx, objInfo.TagID)
if exist {
resp.Title = tag.SlugName
resp.MainTagSlugName = tag.MainTagSlugName
}
}
resp.ObjectType = objInfo.ObjectType
resp.QuestionID = objInfo.QuestionID
resp.AnswerID = objInfo.AnswerID
if len(objInfo.ObjectCreatorUserID) > 0 {
// get object creator user info
userBasicInfo, exist, err := as.userCommon.GetUserBasicInfoByID(ctx, objInfo.ObjectCreatorUserID)
if err != nil {
return nil, err
}
if exist {
resp.Username = userBasicInfo.Username
resp.DisplayName = userBasicInfo.DisplayName
}
}
return resp, nil
}
func (as *ActivityService) getTimelineActivityComment(ctx context.Context, objectID, objectType,
activityType, revisionID string) (comment string) {
if objectType == constant.CommentObjectType {
commentInfo, err := as.commentCommonService.GetComment(ctx, objectID)
if err != nil {
log.Error(err)
} else {
return commentInfo.ParsedText
}
return
}
if activityType == constant.ActEdited {
revision, err := as.revisionService.GetRevision(ctx, revisionID)
if err != nil {
log.Error(err)
} else {
return revision.Log
}
return
}
if activityType == constant.ActClosed {
// only question can be closed
metaInfo, err := as.metaService.GetMetaByObjectIdAndKey(ctx, objectID, entity.QuestionCloseReasonKey)
if err != nil {
log.Error(err)
} else {
closeMsg := &schema.CloseQuestionMeta{}
if err := json.Unmarshal([]byte(metaInfo.Value), closeMsg); err == nil {
return closeMsg.CloseMsg
}
}
}
return ""
}
func (as *ActivityService) formatTimelineUserInfo(ctx context.Context, timeline []*schema.ActObjectTimeline) {
userExist := make(map[string]bool)
userIDs := make([]string, 0)
for _, info := range timeline {
if len(info.UserID) == 0 || userExist[info.UserID] {
continue
}
userIDs = append(userIDs, info.UserID)
}
if len(userIDs) == 0 {
return
}
userInfoMapping, err := as.userCommon.BatchUserBasicInfoByID(ctx, userIDs)
if err != nil {
log.Error(err)
return
}
for _, info := range timeline {
if len(info.UserID) == 0 {
continue
}
if userInfo, ok := userInfoMapping[info.UserID]; ok {
info.Username = userInfo.Username
info.UserDisplayName = userInfo.DisplayName
}
}
}
// GetObjectTimelineDetail get object timeline
func (as *ActivityService) GetObjectTimelineDetail(ctx context.Context, req *schema.GetObjectTimelineDetailReq) (
resp *schema.GetObjectTimelineDetailResp, err error) {
resp = &schema.GetObjectTimelineDetailResp{}
resp.OldRevision, _ = as.getOneObjectDetail(ctx, req.OldRevisionID)
resp.NewRevision, _ = as.getOneObjectDetail(ctx, req.NewRevisionID)
return resp, nil
}
// GetObjectTimelineDetail get object detail
func (as *ActivityService) getOneObjectDetail(ctx context.Context, revisionID string) (
resp *schema.ObjectTimelineDetail, err error) {
resp = &schema.ObjectTimelineDetail{Tags: make([]*schema.ObjectTimelineTag, 0)}
// if request revision is 0, return null object detail.
if revisionID == "0" {
return nil, nil
}
revision, err := as.revisionService.GetRevision(ctx, revisionID)
if err != nil {
log.Warn(err)
return nil, nil
}
objInfo, err := as.objectInfoService.GetInfo(ctx, revision.ObjectID)
if err != nil {
return nil, err
}
switch objInfo.ObjectType {
case constant.QuestionObjectType:
data := &entity.QuestionWithTagsRevision{}
if err = json.Unmarshal([]byte(revision.Content), data); err != nil {
log.Errorf("revision parsing error %s", err)
return resp, nil
}
for _, tag := range data.Tags {
resp.Tags = append(resp.Tags, &schema.ObjectTimelineTag{
SlugName: tag.SlugName,
DisplayName: tag.DisplayName,
MainTagSlugName: tag.MainTagSlugName,
Recommend: tag.Recommend,
Reserved: tag.Reserved,
})
}
resp.Title = data.Title
resp.OriginalText = data.OriginalText
case constant.AnswerObjectType:
data := &entity.Answer{}
if err = json.Unmarshal([]byte(revision.Content), data); err != nil {
log.Errorf("revision parsing error %s", err)
return resp, nil
}
resp.Title = objInfo.Title // answer show question title
resp.OriginalText = data.OriginalText
case constant.TagObjectType:
data := &entity.Tag{}
if err = json.Unmarshal([]byte(revision.Content), data); err != nil {
log.Errorf("revision parsing error %s", err)
return resp, nil
}
resp.Title = data.DisplayName
resp.OriginalText = data.OriginalText
resp.SlugName = data.SlugName
resp.MainTagSlugName = data.MainTagSlugName
default:
log.Errorf("unknown object type %s", objInfo.ObjectType)
}
return resp, nil
}
func formatActivity(activityType string) (isHidden bool, formattedActivityType string) {
if activityType == constant.ActVotedUp ||
activityType == constant.ActVotedDown ||
activityType == constant.ActFollow {
return true, ""
}
if activityType == constant.ActVoteUp {
return false, constant.ActUpVote
}
if activityType == constant.ActVoteDown {
return false, constant.ActDownVote
}
if activityType == constant.ActAccepted {
return false, constant.ActAccept
}
return false, activityType
}

View File

@ -10,9 +10,9 @@ import (
// AnswerActivityRepo answer activity
type AnswerActivityRepo interface {
AcceptAnswer(ctx context.Context,
answerObjID, questionUserID, answerUserID string, isSelf bool) (err error)
answerObjID, questionObjID, questionUserID, answerUserID string, isSelf bool) (err error)
CancelAcceptAnswer(ctx context.Context,
answerObjID, questionUserID, answerUserID string) (err error)
answerObjID, questionObjID, questionUserID, answerUserID string) (err error)
DeleteAnswer(ctx context.Context, answerID string) (err error)
}
@ -38,14 +38,14 @@ func NewAnswerActivityService(
// AcceptAnswer accept answer change activity
func (as *AnswerActivityService) AcceptAnswer(ctx context.Context,
answerObjID, questionUserID, answerUserID string, isSelf bool) (err error) {
return as.answerActivityRepo.AcceptAnswer(ctx, answerObjID, questionUserID, answerUserID, isSelf)
answerObjID, questionObjID, questionUserID, answerUserID string, isSelf bool) (err error) {
return as.answerActivityRepo.AcceptAnswer(ctx, answerObjID, questionObjID, questionUserID, answerUserID, isSelf)
}
// CancelAcceptAnswer cancel accept answer change activity
func (as *AnswerActivityService) CancelAcceptAnswer(ctx context.Context,
answerObjID, questionUserID, answerUserID string) (err error) {
return as.answerActivityRepo.CancelAcceptAnswer(ctx, answerObjID, questionUserID, answerUserID)
answerObjID, questionObjID, questionUserID, answerUserID string) (err error) {
return as.answerActivityRepo.CancelAcceptAnswer(ctx, answerObjID, questionObjID, questionUserID, answerUserID)
}
// DeleteAnswer delete answer change activity

View File

@ -4,6 +4,9 @@ import (
"context"
"github.com/answerdev/answer/internal/entity"
"github.com/answerdev/answer/internal/service/activity_queue"
"github.com/answerdev/answer/pkg/converter"
"github.com/segmentfault/pacman/log"
"xorm.io/xorm"
)
@ -13,4 +16,56 @@ type ActivityRepo interface {
GetActivity(ctx context.Context, session *xorm.Session, objectID, userID string, activityType int) (
existsActivity *entity.Activity, exist bool, err error)
GetUserIDObjectIDActivitySum(ctx context.Context, userID, objectID string) (int, error)
GetActivityTypeByConfigKey(ctx context.Context, configKey string) (activityType int, err error)
AddActivity(ctx context.Context, activity *entity.Activity) (err error)
}
type ActivityCommon struct {
activityRepo ActivityRepo
}
// NewActivityCommon new activity common
func NewActivityCommon(
activityRepo ActivityRepo,
) *ActivityCommon {
activity := &ActivityCommon{
activityRepo: activityRepo,
}
activity.HandleActivity()
return activity
}
// HandleActivity handle activity message
func (ac *ActivityCommon) HandleActivity() {
go func() {
defer func() {
if err := recover(); err != nil {
log.Error(err)
}
}()
for msg := range activity_queue.ActivityQueue {
log.Debugf("received activity %+v", msg)
activityType, err := ac.activityRepo.GetActivityTypeByConfigKey(context.Background(), string(msg.ActivityTypeKey))
if err != nil {
log.Errorf("error getting activity type %s, activity type is %d", err, activityType)
}
act := &entity.Activity{
UserID: msg.UserID,
TriggerUserID: msg.TriggerUserID,
ObjectID: msg.ObjectID,
OriginalObjectID: msg.OriginalObjectID,
ActivityType: activityType,
Cancelled: entity.ActivityAvailable,
}
if len(msg.RevisionID) > 0 {
act.RevisionID = converter.StringToInt64(msg.RevisionID)
}
if err := ac.activityRepo.AddActivity(context.TODO(), act); err != nil {
log.Error(err)
}
}
}()
}

View File

@ -0,0 +1,14 @@
package activity_queue
import (
"github.com/answerdev/answer/internal/schema"
)
var (
ActivityQueue = make(chan *schema.ActivityMsg, 128)
)
// AddActivity add new activity
func AddActivity(msg *schema.ActivityMsg) {
ActivityQueue <- msg
}

View File

@ -68,7 +68,11 @@ func (as *AnswerCommon) ShowFormat(ctx context.Context, data *entity.Answer) *sc
info.VoteCount = data.VoteCount
info.CreateTime = data.CreatedAt.Unix()
info.UpdateTime = data.UpdatedAt.Unix()
if data.UpdatedAt.Unix() < 1 {
info.UpdateTime = 0
}
info.UserID = data.UserID
info.UpdateUserID = data.LastEditUserID
return &info
}
@ -80,7 +84,11 @@ func (as *AnswerCommon) AdminShowFormat(ctx context.Context, data *entity.Answer
info.VoteCount = data.VoteCount
info.CreateTime = data.CreatedAt.Unix()
info.UpdateTime = data.UpdatedAt.Unix()
if data.UpdatedAt.Unix() < 1 {
info.UpdateTime = 0
}
info.UserID = data.UserID
info.UpdateUserID = data.LastEditUserID
info.Description = htmltext.FetchExcerpt(data.ParsedText, "...", 240)
return &info
}

View File

@ -7,18 +7,18 @@ import (
"time"
"github.com/answerdev/answer/internal/base/constant"
"github.com/answerdev/answer/internal/service/activity"
"github.com/answerdev/answer/internal/service/activity_common"
"github.com/answerdev/answer/internal/service/notice_queue"
"github.com/answerdev/answer/internal/service/revision_common"
"github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/internal/entity"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/activity"
"github.com/answerdev/answer/internal/service/activity_common"
"github.com/answerdev/answer/internal/service/activity_queue"
answercommon "github.com/answerdev/answer/internal/service/answer_common"
collectioncommon "github.com/answerdev/answer/internal/service/collection_common"
"github.com/answerdev/answer/internal/service/notice_queue"
"github.com/answerdev/answer/internal/service/permission"
questioncommon "github.com/answerdev/answer/internal/service/question_common"
"github.com/answerdev/answer/internal/service/revision_common"
usercommon "github.com/answerdev/answer/internal/service/user_common"
"github.com/segmentfault/pacman/errors"
"github.com/segmentfault/pacman/log"
@ -73,27 +73,29 @@ func (as *AnswerService) RemoveAnswer(ctx context.Context, req *schema.RemoveAns
if !exist {
return nil
}
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)
if !req.IsAdmin {
if answerInfo.UserID != req.UserID {
return errors.BadRequest(reason.AnswerCannotDeleted)
}
if answerInfo.VoteCount > 0 {
return errors.BadRequest(reason.AnswerCannotDeleted)
}
if answerInfo.Adopted == schema.AnswerAdoptedEnable {
return errors.BadRequest(reason.AnswerCannotDeleted)
}
questionInfo, exist, err := as.questionRepo.GetQuestion(ctx, answerInfo.QuestionID)
if err != nil {
return errors.BadRequest(reason.AnswerCannotDeleted)
}
if !exist {
return errors.BadRequest(reason.AnswerCannotDeleted)
}
if questionInfo.AnswerCount > 1 {
return errors.BadRequest(reason.AnswerCannotDeleted)
}
if questionInfo.AcceptedAnswerID != "" {
return errors.BadRequest(reason.AnswerCannotDeleted)
}
}
// user add question count
@ -115,6 +117,12 @@ func (as *AnswerService) RemoveAnswer(ctx context.Context, req *schema.RemoveAns
if err != nil {
log.Errorf("delete answer activity change failed: %s", err.Error())
}
activity_queue.AddActivity(&schema.ActivityMsg{
UserID: req.UserID,
ObjectID: answerInfo.ID,
OriginalObjectID: answerInfo.ID,
ActivityTypeKey: constant.ActAnswerDeleted,
})
return
}
@ -126,7 +134,6 @@ func (as *AnswerService) Insert(ctx context.Context, req *schema.AnswerAddReq) (
if !exist {
return "", errors.BadRequest(reason.QuestionNotFound)
}
now := time.Now()
insertData := new(entity.Answer)
insertData.UserID = req.UserID
insertData.OriginalText = req.Content
@ -134,8 +141,9 @@ func (as *AnswerService) Insert(ctx context.Context, req *schema.AnswerAddReq) (
insertData.Adopted = schema.AnswerAdoptedFailed
insertData.QuestionID = req.QuestionID
insertData.RevisionID = "0"
insertData.LastEditUserID = "0"
insertData.Status = entity.AnswerStatusAvailable
insertData.UpdatedAt = now
//insertData.UpdatedAt = now
if err = as.answerRepo.AddAnswer(ctx, insertData); err != nil {
return "", err
}
@ -164,15 +172,40 @@ func (as *AnswerService) Insert(ctx context.Context, req *schema.AnswerAddReq) (
}
infoJSON, _ := json.Marshal(insertData)
revisionDTO.Content = string(infoJSON)
err = as.revisionService.AddRevision(ctx, revisionDTO, true)
revisionID, err := as.revisionService.AddRevision(ctx, revisionDTO, true)
if err != nil {
return insertData.ID, err
}
as.notificationAnswerTheQuestion(ctx, questionInfo.UserID, insertData.ID, req.UserID)
activity_queue.AddActivity(&schema.ActivityMsg{
UserID: insertData.UserID,
ObjectID: insertData.ID,
OriginalObjectID: insertData.ID,
ActivityTypeKey: constant.ActAnswerAnswered,
RevisionID: revisionID,
})
activity_queue.AddActivity(&schema.ActivityMsg{
UserID: insertData.UserID,
ObjectID: insertData.ID,
OriginalObjectID: questionInfo.ID,
ActivityTypeKey: constant.ActQuestionAnswered,
})
return insertData.ID, nil
}
func (as *AnswerService) Update(ctx context.Context, req *schema.AnswerUpdateReq) (string, error) {
//req.NoNeedReview //true 不需要审核
var canUpdate bool
_, existUnreviewed, err := as.revisionService.ExistUnreviewedByObjectID(ctx, req.ID)
if err != nil {
return "", err
}
if existUnreviewed {
err = errors.BadRequest(reason.AnswerCannotUpdate)
return "", err
}
questionInfo, exist, err := as.questionRepo.GetQuestion(ctx, req.QuestionID)
if err != nil {
return "", err
@ -180,34 +213,75 @@ func (as *AnswerService) Update(ctx context.Context, req *schema.AnswerUpdateReq
if !exist {
return "", errors.BadRequest(reason.QuestionNotFound)
}
answerInfo, exist, err := as.answerRepo.GetByID(ctx, req.ID)
if err != nil {
return "", err
}
if !exist {
return "", nil
}
//If the content is the same, ignore it
if answerInfo.OriginalText == req.Content {
return "", nil
}
now := time.Now()
insertData := new(entity.Answer)
insertData.ID = req.ID
insertData.UserID = answerInfo.UserID
insertData.QuestionID = req.QuestionID
insertData.UserID = req.UserID
insertData.OriginalText = req.Content
insertData.ParsedText = req.HTML
insertData.UpdatedAt = now
if err = as.answerRepo.UpdateAnswer(ctx, insertData, []string{"original_text", "parsed_text", "update_time"}); err != nil {
return "", err
}
err = as.questionCommon.UpdataPostTime(ctx, req.QuestionID)
if err != nil {
return insertData.ID, err
insertData.LastEditUserID = "0"
if answerInfo.UserID != req.UserID {
insertData.LastEditUserID = req.UserID
}
revisionDTO := &schema.AddRevisionDTO{
UserID: req.UserID,
ObjectID: req.ID,
Title: "",
Log: req.EditSummary,
}
if req.NoNeedReview || answerInfo.UserID == req.UserID {
canUpdate = true
}
if !canUpdate {
revisionDTO.Status = entity.RevisionUnreviewedStatus
} else {
if err = as.answerRepo.UpdateAnswer(ctx, insertData, []string{"original_text", "parsed_text", "updated_at", "last_edit_user_id"}); err != nil {
return "", err
}
err = as.questionCommon.UpdataPostTime(ctx, req.QuestionID)
if err != nil {
return insertData.ID, err
}
as.notificationUpdateAnswer(ctx, questionInfo.UserID, insertData.ID, req.UserID)
revisionDTO.Status = entity.RevisionReviewPassStatus
}
infoJSON, _ := json.Marshal(insertData)
revisionDTO.Content = string(infoJSON)
err = as.revisionService.AddRevision(ctx, revisionDTO, true)
revisionID, err := as.revisionService.AddRevision(ctx, revisionDTO, true)
if err != nil {
return insertData.ID, err
}
as.notificationUpdateAnswer(ctx, questionInfo.UserID, insertData.ID, req.UserID)
if canUpdate {
activity_queue.AddActivity(&schema.ActivityMsg{
UserID: insertData.UserID,
ObjectID: insertData.ID,
OriginalObjectID: insertData.ID,
ActivityTypeKey: constant.ActAnswerEdited,
RevisionID: revisionID,
})
}
return insertData.ID, nil
}
@ -276,14 +350,14 @@ func (as *AnswerService) updateAnswerRank(ctx context.Context, userID string,
// if this question is already been answered, should cancel old answer rank
if oldAnswerInfo != nil {
err := as.answerActivityService.CancelAcceptAnswer(
ctx, questionInfo.AcceptedAnswerID, questionInfo.UserID, oldAnswerInfo.UserID)
ctx, questionInfo.AcceptedAnswerID, questionInfo.ID, questionInfo.UserID, oldAnswerInfo.UserID)
if err != nil {
log.Error(err)
}
}
if newAnswerInfo.ID != "" {
err := as.answerActivityService.AcceptAnswer(
ctx, newAnswerInfo.ID, questionInfo.UserID, newAnswerInfo.UserID, newAnswerInfo.UserID == userID)
ctx, newAnswerInfo.ID, questionInfo.ID, questionInfo.UserID, newAnswerInfo.UserID, newAnswerInfo.UserID == userID)
if err != nil {
log.Error(err)
}
@ -302,13 +376,22 @@ func (as *AnswerService) Get(ctx context.Context, answerID, loginUserID string)
return nil, nil, has, err
}
// todo UserFunc
userinfo, has, err := as.userCommon.GetUserBasicInfoByID(ctx, answerInfo.UserID)
userIds := make([]string, 0)
userIds = append(userIds, answerInfo.UserID)
userIds = append(userIds, answerInfo.LastEditUserID)
userInfoMap, err := as.userCommon.BatchUserBasicInfoByID(ctx, userIds)
if err != nil {
return nil, nil, has, err
}
if has {
info.UserInfo = userinfo
info.UpdateUserInfo = userinfo
_, ok := userInfoMap[answerInfo.UserID]
if ok {
info.UserInfo = userInfoMap[answerInfo.UserID]
}
_, ok = userInfoMap[answerInfo.LastEditUserID]
if ok {
info.UpdateUserInfo = userInfoMap[answerInfo.LastEditUserID]
}
if loginUserID == "" {
@ -321,7 +404,7 @@ func (as *AnswerService) Get(ctx context.Context, answerID, loginUserID string)
if err != nil {
log.Error("CollectionFunc.SearchObjectCollected error", err)
}
_, ok := CollectedMap[answerInfo.ID]
_, ok = CollectedMap[answerInfo.ID]
if ok {
info.Collected = true
}
@ -329,12 +412,12 @@ func (as *AnswerService) Get(ctx context.Context, answerID, loginUserID string)
return info, questionInfo, has, nil
}
func (as *AnswerService) AdminSetAnswerStatus(ctx context.Context, answerID string, setStatusStr string) error {
setStatus, ok := entity.CmsAnswerSearchStatus[setStatusStr]
func (as *AnswerService) AdminSetAnswerStatus(ctx context.Context, req *schema.AdminSetAnswerStatusRequest) error {
setStatus, ok := entity.CmsAnswerSearchStatus[req.StatusStr]
if !ok {
return fmt.Errorf("question status does not exist")
}
answerInfo, exist, err := as.answerRepo.GetAnswer(ctx, answerID)
answerInfo, exist, err := as.answerRepo.GetAnswer(ctx, req.AnswerID)
if err != nil {
return err
}
@ -348,9 +431,16 @@ func (as *AnswerService) AdminSetAnswerStatus(ctx context.Context, answerID stri
}
if setStatus == entity.AnswerStatusDeleted {
err = as.answerActivityService.DeleteQuestion(ctx, answerInfo.ID, answerInfo.CreatedAt, answerInfo.VoteCount)
err = as.answerActivityService.DeleteAnswer(ctx, answerInfo.ID, answerInfo.CreatedAt, answerInfo.VoteCount)
if err != nil {
log.Errorf("admin delete question then rank rollback error %s", err.Error())
} else {
activity_queue.AddActivity(&schema.ActivityMsg{
UserID: req.UserID,
ObjectID: answerInfo.ID,
OriginalObjectID: answerInfo.ID,
ActivityTypeKey: constant.ActAnswerDeleted,
})
}
}
@ -366,39 +456,40 @@ func (as *AnswerService) AdminSetAnswerStatus(ctx context.Context, answerID stri
return nil
}
func (as *AnswerService) SearchList(ctx context.Context, search *schema.AnswerList) ([]*schema.AnswerInfo, int64, error) {
func (as *AnswerService) SearchList(ctx context.Context, req *schema.AnswerListReq) ([]*schema.AnswerInfo, int64, error) {
list := make([]*schema.AnswerInfo, 0)
dbSearch := entity.AnswerSearch{}
dbSearch.QuestionID = search.QuestionID
dbSearch.Page = search.Page
dbSearch.PageSize = search.PageSize
dbSearch.Order = search.Order
dblist, count, err := as.answerRepo.SearchList(ctx, &dbSearch)
dbSearch.QuestionID = req.QuestionID
dbSearch.Page = req.Page
dbSearch.PageSize = req.PageSize
dbSearch.Order = req.Order
answerOriginalList, count, err := as.answerRepo.SearchList(ctx, &dbSearch)
if err != nil {
return list, count, err
}
AnswerList, err := as.SearchFormatInfo(ctx, dblist, search.LoginUserID)
answerList, err := as.SearchFormatInfo(ctx, answerOriginalList, req)
if err != nil {
return AnswerList, count, err
return answerList, count, err
}
return AnswerList, count, nil
return answerList, count, nil
}
func (as *AnswerService) SearchFormatInfo(ctx context.Context, dblist []*entity.Answer, loginUserID string) ([]*schema.AnswerInfo, error) {
func (as *AnswerService) SearchFormatInfo(ctx context.Context, answers []*entity.Answer, req *schema.AnswerListReq) (
[]*schema.AnswerInfo, error) {
list := make([]*schema.AnswerInfo, 0)
objectIds := make([]string, 0)
userIds := make([]string, 0)
for _, dbitem := range dblist {
item := as.ShowFormat(ctx, dbitem)
objectIDs := make([]string, 0)
userIDs := make([]string, 0)
for _, info := range answers {
item := as.ShowFormat(ctx, info)
list = append(list, item)
objectIds = append(objectIds, dbitem.ID)
userIds = append(userIds, dbitem.UserID)
if loginUserID != "" {
// item.VoteStatus = as.activityFunc.GetVoteStatus(ctx, item.TagID, loginUserId)
item.VoteStatus = as.voteRepo.GetVoteStatus(ctx, item.ID, loginUserID)
objectIDs = append(objectIDs, info.ID)
userIDs = append(userIDs, info.UserID)
userIDs = append(userIDs, info.LastEditUserID)
if req.UserID != "" {
item.VoteStatus = as.voteRepo.GetVoteStatus(ctx, item.ID, req.UserID)
}
}
userInfoMap, err := as.userCommon.BatchUserBasicInfoByID(ctx, userIds)
userInfoMap, err := as.userCommon.BatchUserBasicInfoByID(ctx, userIDs)
if err != nil {
return list, err
}
@ -406,30 +497,32 @@ func (as *AnswerService) SearchFormatInfo(ctx context.Context, dblist []*entity.
_, ok := userInfoMap[item.UserID]
if ok {
item.UserInfo = userInfoMap[item.UserID]
item.UpdateUserInfo = userInfoMap[item.UserID]
}
_, ok = userInfoMap[item.UpdateUserID]
if ok {
item.UpdateUserInfo = userInfoMap[item.UpdateUserID]
}
}
if loginUserID == "" {
if req.UserID == "" {
return list, nil
}
CollectedMap, err := as.collectionCommon.SearchObjectCollected(ctx, loginUserID, objectIds)
searchObjectCollected, err := as.collectionCommon.SearchObjectCollected(ctx, req.UserID, objectIDs)
if err != nil {
log.Error("CollectionFunc.SearchObjectCollected error", err)
return nil, err
}
for _, item := range list {
_, ok := CollectedMap[item.ID]
_, ok := searchObjectCollected[item.ID]
if ok {
item.Collected = true
}
}
for _, item := range list {
item.MemberActions = permission.GetAnswerPermission(loginUserID, item.UserID)
item.MemberActions = permission.GetAnswerPermission(ctx, req.UserID, item.UserID, req.CanEdit, req.CanDelete)
}
return list, nil
}

View File

@ -40,7 +40,7 @@ func (as *AuthService) GetUserCacheInfo(ctx context.Context, accessToken string)
}
cacheInfo, _ := as.authRepo.GetUserStatus(ctx, userCacheInfo.UserID)
if cacheInfo != nil {
log.Infof("user status updated: %+v", cacheInfo)
log.Debugf("user status updated: %+v", cacheInfo)
userCacheInfo.UserStatus = cacheInfo.UserStatus
userCacheInfo.EmailStatus = cacheInfo.EmailStatus
userCacheInfo.IsAdmin = cacheInfo.IsAdmin

View File

@ -9,9 +9,10 @@ import (
"github.com/answerdev/answer/internal/entity"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/activity_common"
"github.com/answerdev/answer/internal/service/activity_queue"
"github.com/answerdev/answer/internal/service/comment_common"
"github.com/answerdev/answer/internal/service/notice_queue"
object_info "github.com/answerdev/answer/internal/service/object_info"
"github.com/answerdev/answer/internal/service/object_info"
"github.com/answerdev/answer/internal/service/permission"
usercommon "github.com/answerdev/answer/internal/service/user_common"
"github.com/jinzhu/copier"
@ -111,9 +112,9 @@ func (cs *CommentService) AddComment(ctx context.Context, req *schema.AddComment
}
if objInfo.ObjectType == constant.QuestionObjectType {
cs.notificationQuestionComment(ctx, objInfo.ObjectCreator, comment.ID, req.UserID)
cs.notificationQuestionComment(ctx, objInfo.ObjectCreatorUserID, comment.ID, req.UserID)
} else if objInfo.ObjectType == constant.AnswerObjectType {
cs.notificationAnswerComment(ctx, objInfo.ObjectCreator, comment.ID, req.UserID)
cs.notificationAnswerComment(ctx, objInfo.ObjectCreatorUserID, comment.ID, req.UserID)
}
if len(req.MentionUsernameList) > 0 {
cs.notificationMention(ctx, req.MentionUsernameList, comment.ID, req.UserID)
@ -121,7 +122,7 @@ func (cs *CommentService) AddComment(ctx context.Context, req *schema.AddComment
resp = &schema.GetCommentResp{}
resp.SetFromComment(comment)
resp.MemberActions = permission.GetCommentPermission(req.UserID, resp.UserID)
resp.MemberActions = permission.GetCommentPermission(ctx, req.UserID, resp.UserID, req.CanEdit, req.CanDelete)
// get reply user info
if len(resp.ReplyUserID) > 0 {
@ -148,6 +149,20 @@ func (cs *CommentService) AddComment(ctx context.Context, req *schema.AddComment
resp.UserAvatar = userInfo.Avatar
resp.UserStatus = userInfo.Status
}
activityMsg := &schema.ActivityMsg{
UserID: comment.UserID,
ObjectID: comment.ID,
OriginalObjectID: req.ObjectID,
ActivityTypeKey: constant.ActQuestionCommented,
}
switch objInfo.ObjectType {
case constant.QuestionObjectType:
activityMsg.ActivityTypeKey = constant.ActQuestionCommented
case constant.AnswerObjectType:
activityMsg.ActivityTypeKey = constant.ActAnswerCommented
}
activity_queue.AddActivity(activityMsg)
return resp, nil
}
@ -222,7 +237,7 @@ func (cs *CommentService) GetComment(ctx context.Context, req *schema.GetComment
// check if current user vote this comment
resp.IsVote = cs.checkIsVote(ctx, req.UserID, resp.CommentID)
resp.MemberActions = permission.GetCommentPermission(req.UserID, resp.UserID)
resp.MemberActions = permission.GetCommentPermission(ctx, req.UserID, resp.UserID, req.CanEdit, req.CanDelete)
return resp, nil
}
@ -240,54 +255,88 @@ func (cs *CommentService) GetCommentWithPage(ctx context.Context, req *schema.Ge
}
resp := make([]*schema.GetCommentResp, 0)
for _, comment := range commentList {
commentResp := &schema.GetCommentResp{
CommentID: comment.ID,
CreatedAt: comment.CreatedAt.Unix(),
UserID: comment.UserID,
ReplyUserID: comment.GetReplyUserID(),
ReplyCommentID: comment.GetReplyCommentID(),
ObjectID: comment.ObjectID,
VoteCount: comment.VoteCount,
OriginalText: comment.OriginalText,
ParsedText: comment.ParsedText,
commentResp, err := cs.convertCommentEntity2Resp(ctx, req, comment)
if err != nil {
return nil, err
}
// get comment user info
if len(commentResp.UserID) > 0 {
commentUser, exist, err := cs.userCommon.GetUserBasicInfoByID(ctx, commentResp.UserID)
if err != nil {
return nil, err
}
if exist {
commentResp.Username = commentUser.Username
commentResp.UserDisplayName = commentUser.DisplayName
commentResp.UserAvatar = commentUser.Avatar
commentResp.UserStatus = commentUser.Status
}
}
// get reply user info
if len(commentResp.ReplyUserID) > 0 {
replyUser, exist, err := cs.userCommon.GetUserBasicInfoByID(ctx, commentResp.ReplyUserID)
if err != nil {
return nil, err
}
if exist {
commentResp.ReplyUsername = replyUser.Username
commentResp.ReplyUserDisplayName = replyUser.DisplayName
commentResp.ReplyUserStatus = replyUser.Status
}
}
// check if current user vote this comment
commentResp.IsVote = cs.checkIsVote(ctx, req.UserID, commentResp.CommentID)
commentResp.MemberActions = permission.GetCommentPermission(req.UserID, commentResp.UserID)
resp = append(resp, commentResp)
}
// if user request the specific comment, add it if not exist.
if len(req.CommentID) > 0 {
commentExist := false
for _, t := range resp {
if t.CommentID == req.CommentID {
commentExist = true
break
}
}
if !commentExist {
comment, exist, err := cs.commentCommonRepo.GetComment(ctx, req.CommentID)
if err != nil {
return nil, err
}
if exist && comment.ObjectID == req.ObjectID {
commentResp, err := cs.convertCommentEntity2Resp(ctx, req, comment)
if err != nil {
return nil, err
}
resp = append(resp, commentResp)
}
}
}
return pager.NewPageModel(total, resp), nil
}
func (cs *CommentService) convertCommentEntity2Resp(ctx context.Context, req *schema.GetCommentWithPageReq,
comment *entity.Comment) (commentResp *schema.GetCommentResp, err error) {
commentResp = &schema.GetCommentResp{
CommentID: comment.ID,
CreatedAt: comment.CreatedAt.Unix(),
UserID: comment.UserID,
ReplyUserID: comment.GetReplyUserID(),
ReplyCommentID: comment.GetReplyCommentID(),
ObjectID: comment.ObjectID,
VoteCount: comment.VoteCount,
OriginalText: comment.OriginalText,
ParsedText: comment.ParsedText,
}
// get comment user info
if len(commentResp.UserID) > 0 {
commentUser, exist, err := cs.userCommon.GetUserBasicInfoByID(ctx, commentResp.UserID)
if err != nil {
return nil, err
}
if exist {
commentResp.Username = commentUser.Username
commentResp.UserDisplayName = commentUser.DisplayName
commentResp.UserAvatar = commentUser.Avatar
commentResp.UserStatus = commentUser.Status
}
}
// get reply user info
if len(commentResp.ReplyUserID) > 0 {
replyUser, exist, err := cs.userCommon.GetUserBasicInfoByID(ctx, commentResp.ReplyUserID)
if err != nil {
return nil, err
}
if exist {
commentResp.ReplyUsername = replyUser.Username
commentResp.ReplyUserDisplayName = replyUser.DisplayName
commentResp.ReplyUserStatus = replyUser.Status
}
}
// check if current user vote this comment
commentResp.IsVote = cs.checkIsVote(ctx, req.UserID, commentResp.CommentID)
commentResp.MemberActions = permission.GetCommentPermission(ctx,
req.UserID, commentResp.UserID, req.CanEdit, req.CanDelete)
return commentResp, nil
}
func (cs *CommentService) checkCommentWhetherOwner(ctx context.Context, userID, commentID string) error {
// check comment if user self
comment, exist, err := cs.commentCommonRepo.GetComment(ctx, commentID)

View File

@ -11,6 +11,8 @@ import (
"github.com/answerdev/answer/internal/base/translator"
"github.com/answerdev/answer/internal/schema"
notficationcommon "github.com/answerdev/answer/internal/service/notification_common"
"github.com/answerdev/answer/internal/service/revision_common"
"github.com/jinzhu/copier"
"github.com/segmentfault/pacman/i18n"
"github.com/segmentfault/pacman/log"
)
@ -20,24 +22,28 @@ type NotificationService struct {
data *data.Data
notificationRepo notficationcommon.NotificationRepo
notificationCommon *notficationcommon.NotificationCommon
revisionService *revision_common.RevisionService
}
func NewNotificationService(
data *data.Data,
notificationRepo notficationcommon.NotificationRepo,
notificationCommon *notficationcommon.NotificationCommon,
revisionService *revision_common.RevisionService,
) *NotificationService {
return &NotificationService{
data: data,
notificationRepo: notificationRepo,
notificationCommon: notificationCommon,
revisionService: revisionService,
}
}
func (ns *NotificationService) GetRedDot(ctx context.Context, userID string) (*schema.RedDot, error) {
func (ns *NotificationService) GetRedDot(ctx context.Context, req *schema.GetRedDot) (*schema.RedDot, error) {
redBot := &schema.RedDot{}
inboxKey := fmt.Sprintf("answer_RedDot_%d_%s", schema.NotificationTypeInbox, userID)
achievementKey := fmt.Sprintf("answer_RedDot_%d_%s", schema.NotificationTypeAchievement, userID)
inboxKey := fmt.Sprintf("answer_RedDot_%d_%s", schema.NotificationTypeInbox, req.UserID)
achievementKey := fmt.Sprintf("answer_RedDot_%d_%s", schema.NotificationTypeAchievement, req.UserID)
inboxValue, err := ns.data.Cache.GetInt64(ctx, inboxKey)
if err != nil {
redBot.Inbox = 0
@ -50,19 +56,32 @@ func (ns *NotificationService) GetRedDot(ctx context.Context, userID string) (*s
} else {
redBot.Achievement = achievementValue
}
revisionCount := &schema.RevisionSearch{}
_ = copier.Copy(revisionCount, req)
if req.CanReviewAnswer || req.CanReviewQuestion || req.CanReviewTag {
redBot.CanRevision = true
revisionCountNum, err := ns.revisionService.GetUnreviewedRevisionCount(ctx, revisionCount)
if err != nil {
return redBot, err
}
redBot.Revision = revisionCountNum
}
return redBot, nil
}
func (ns *NotificationService) ClearRedDot(ctx context.Context, userID string, botTypeStr string) (*schema.RedDot, error) {
botType, ok := schema.NotificationType[botTypeStr]
func (ns *NotificationService) ClearRedDot(ctx context.Context, req *schema.NotificationClearRequest) (*schema.RedDot, error) {
botType, ok := schema.NotificationType[req.TypeStr]
if ok {
key := fmt.Sprintf("answer_RedDot_%d_%s", botType, userID)
key := fmt.Sprintf("answer_RedDot_%d_%s", botType, req.UserID)
err := ns.data.Cache.Del(ctx, key)
if err != nil {
log.Error("ClearRedDot del cache error", err.Error())
}
}
return ns.GetRedDot(ctx, userID)
getRedDotreq := &schema.GetRedDot{}
_ = copier.Copy(getRedDotreq, req)
return ns.GetRedDot(ctx, getRedDotreq)
}
func (ns *NotificationService) ClearUnRead(ctx context.Context, userID string, botTypeStr string) error {

View File

@ -20,6 +20,7 @@ type ObjService struct {
questionRepo questioncommon.QuestionRepo
commentRepo comment_common.CommentCommonRepo
tagRepo tagcommon.TagCommonRepo
tagCommon *tagcommon.TagCommonService
}
// NewObjService new object service
@ -27,14 +28,92 @@ func NewObjService(
answerRepo answercommon.AnswerRepo,
questionRepo questioncommon.QuestionRepo,
commentRepo comment_common.CommentCommonRepo,
tagRepo tagcommon.TagCommonRepo) *ObjService {
tagRepo tagcommon.TagCommonRepo,
tagCommon *tagcommon.TagCommonService,
) *ObjService {
return &ObjService{
answerRepo: answerRepo,
questionRepo: questionRepo,
commentRepo: commentRepo,
tagRepo: tagRepo,
tagCommon: tagCommon,
}
}
func (os *ObjService) GetUnreviewedRevisionInfo(ctx context.Context, objectID string) (objInfo *schema.UnreviewedRevisionInfoInfo, err error) {
objInfo = &schema.UnreviewedRevisionInfoInfo{}
objectType, err := obj.GetObjectTypeStrByObjectID(objectID)
if err != nil {
return nil, err
}
switch objectType {
case constant.QuestionObjectType:
questionInfo, exist, err := os.questionRepo.GetQuestion(ctx, objectID)
if err != nil {
return nil, err
}
if !exist {
break
}
taglist, err := os.tagCommon.GetObjectEntityTag(ctx, objectID)
if err != nil {
return nil, err
}
os.tagCommon.TagsFormatRecommendAndReserved(ctx, taglist)
tags, err := os.tagCommon.TagFormat(ctx, taglist)
if err != nil {
return nil, err
}
objInfo = &schema.UnreviewedRevisionInfoInfo{
ObjectID: questionInfo.ID,
Title: questionInfo.Title,
Content: questionInfo.OriginalText,
Html: questionInfo.ParsedText,
Tags: tags,
}
case constant.AnswerObjectType:
answerInfo, exist, err := os.answerRepo.GetAnswer(ctx, objectID)
if err != nil {
return nil, err
}
if !exist {
break
}
questionInfo, exist, err := os.questionRepo.GetQuestion(ctx, answerInfo.QuestionID)
if err != nil {
return nil, err
}
if !exist {
break
}
objInfo = &schema.UnreviewedRevisionInfoInfo{
ObjectID: answerInfo.ID,
Title: questionInfo.Title,
Content: answerInfo.OriginalText,
Html: answerInfo.ParsedText,
}
case constant.TagObjectType:
tagInfo, exist, err := os.tagRepo.GetTagByID(ctx, objectID, true)
if err != nil {
return nil, err
}
if !exist {
break
}
objInfo = &schema.UnreviewedRevisionInfoInfo{
ObjectID: tagInfo.ID,
Title: tagInfo.SlugName,
Content: tagInfo.OriginalText,
Html: tagInfo.ParsedText,
}
}
if objInfo == nil {
err = errors.BadRequest(reason.ObjectNotFound)
}
return objInfo, err
}
// GetInfo get object simple information
func (os *ObjService) GetInfo(ctx context.Context, objectID string) (objInfo *schema.SimpleObjectInfo, err error) {
@ -52,12 +131,12 @@ func (os *ObjService) GetInfo(ctx context.Context, objectID string) (objInfo *sc
break
}
objInfo = &schema.SimpleObjectInfo{
ObjectID: questionInfo.ID,
ObjectCreator: questionInfo.UserID,
QuestionID: questionInfo.ID,
ObjectType: objectType,
Title: questionInfo.Title,
Content: questionInfo.ParsedText, // todo trim
ObjectID: questionInfo.ID,
ObjectCreatorUserID: questionInfo.UserID,
QuestionID: questionInfo.ID,
ObjectType: objectType,
Title: questionInfo.Title,
Content: questionInfo.ParsedText, // todo trim
}
case constant.AnswerObjectType:
answerInfo, exist, err := os.answerRepo.GetAnswer(ctx, objectID)
@ -72,13 +151,13 @@ func (os *ObjService) GetInfo(ctx context.Context, objectID string) (objInfo *sc
return nil, err
}
objInfo = &schema.SimpleObjectInfo{
ObjectID: answerInfo.ID,
ObjectCreator: answerInfo.UserID,
QuestionID: answerInfo.QuestionID,
AnswerID: answerInfo.ID,
ObjectType: objectType,
Title: questionInfo.Title, // this should be question title
Content: answerInfo.ParsedText, // todo trim
ObjectID: answerInfo.ID,
ObjectCreatorUserID: answerInfo.UserID,
QuestionID: answerInfo.QuestionID,
AnswerID: answerInfo.ID,
ObjectType: objectType,
Title: questionInfo.Title, // this should be question title
Content: answerInfo.ParsedText, // todo trim
}
case constant.CommentObjectType:
commentInfo, exist, err := os.commentRepo.GetComment(ctx, objectID)
@ -89,11 +168,11 @@ func (os *ObjService) GetInfo(ctx context.Context, objectID string) (objInfo *sc
break
}
objInfo = &schema.SimpleObjectInfo{
ObjectID: commentInfo.ID,
ObjectCreator: commentInfo.UserID,
ObjectType: objectType,
Content: commentInfo.ParsedText, // todo trim
CommentID: commentInfo.ID,
ObjectID: commentInfo.ID,
ObjectCreatorUserID: commentInfo.UserID,
ObjectType: objectType,
Content: commentInfo.ParsedText, // todo trim
CommentID: commentInfo.ID,
}
if len(commentInfo.QuestionID) > 0 {
questionInfo, exist, err := os.questionRepo.GetQuestion(ctx, commentInfo.QuestionID)
@ -113,7 +192,7 @@ func (os *ObjService) GetInfo(ctx context.Context, objectID string) (objInfo *sc
}
}
case constant.TagObjectType:
tagInfo, exist, err := os.tagRepo.GetTagByID(ctx, objectID)
tagInfo, exist, err := os.tagRepo.GetTagByID(ctx, objectID, true)
if err != nil {
return nil, err
}

View File

@ -1,9 +1,13 @@
package permission
import "github.com/answerdev/answer/internal/schema"
import (
"context"
// TODO: There is currently no permission management
func GetCommentPermission(userID string, commentCreatorUserID string) (
"github.com/answerdev/answer/internal/schema"
)
// GetCommentPermission get comment permission
func GetCommentPermission(ctx context.Context, userID string, creatorUserID string, canEdit, canDelete bool) (
actions []*schema.PermissionMemberAction) {
actions = make([]*schema.PermissionMemberAction, 0)
if len(userID) > 0 {
@ -13,44 +17,48 @@ func GetCommentPermission(userID string, commentCreatorUserID string) (
Type: "reason",
})
}
if userID != commentCreatorUserID {
return actions
}
actions = append(actions, []*schema.PermissionMemberAction{
{
if canEdit || userID == creatorUserID {
actions = append(actions, &schema.PermissionMemberAction{
Action: "edit",
Name: "Edit",
Type: "edit",
},
{
})
}
if canDelete || userID == creatorUserID {
actions = append(actions, &schema.PermissionMemberAction{
Action: "delete",
Name: "Delete",
Type: "reason",
},
}...)
})
}
return actions
}
func GetTagPermission(userID string, tagCreatorUserID string) (
// GetTagPermission get tag permission
func GetTagPermission(ctx context.Context, canEdit, canDelete bool) (
actions []*schema.PermissionMemberAction) {
if userID != tagCreatorUserID {
return []*schema.PermissionMemberAction{}
}
return []*schema.PermissionMemberAction{
{
actions = make([]*schema.PermissionMemberAction, 0)
if canEdit {
actions = append(actions, &schema.PermissionMemberAction{
Action: "edit",
Name: "Edit",
Type: "edit",
},
{
})
}
if canDelete {
actions = append(actions, &schema.PermissionMemberAction{
Action: "delete",
Name: "Delete",
Type: "reason",
},
})
}
return actions
}
func GetAnswerPermission(userID string, answerAuthID string) (
// GetAnswerPermission get answer permission
func GetAnswerPermission(ctx context.Context, userID string, creatorUserID string, canEdit, canDelete bool) (
actions []*schema.PermissionMemberAction) {
actions = make([]*schema.PermissionMemberAction, 0)
if len(userID) > 0 {
@ -60,25 +68,26 @@ func GetAnswerPermission(userID string, answerAuthID string) (
Type: "reason",
})
}
if userID != answerAuthID {
return actions
}
actions = append(actions, []*schema.PermissionMemberAction{
{
if canEdit || userID == creatorUserID {
actions = append(actions, &schema.PermissionMemberAction{
Action: "edit",
Name: "Edit",
Type: "edit",
},
{
})
}
if canDelete || userID == creatorUserID {
actions = append(actions, &schema.PermissionMemberAction{
Action: "delete",
Name: "Delete",
Type: "confirm",
},
}...)
})
}
return actions
}
func GetQuestionPermission(userID string, questionAuthID string) (
// GetQuestionPermission get question permission
func GetQuestionPermission(ctx context.Context, userID string, creatorUserID string, canEdit, canDelete, canClose bool) (
actions []*schema.PermissionMemberAction) {
actions = make([]*schema.PermissionMemberAction, 0)
if len(userID) > 0 {
@ -88,25 +97,40 @@ func GetQuestionPermission(userID string, questionAuthID string) (
Type: "reason",
})
}
if userID != questionAuthID {
return actions
}
actions = append(actions, []*schema.PermissionMemberAction{
{
if canEdit || userID == creatorUserID {
actions = append(actions, &schema.PermissionMemberAction{
Action: "edit",
Name: "Edit",
Type: "edit",
},
{
})
}
if canClose {
actions = append(actions, &schema.PermissionMemberAction{
Action: "close",
Name: "Close",
Type: "confirm",
},
{
})
}
if canDelete || userID == creatorUserID {
actions = append(actions, &schema.PermissionMemberAction{
Action: "delete",
Name: "Delete",
Type: "confirm",
},
}...)
})
}
return actions
}
// GetTagSynonymPermission get tag synonym permission
func GetTagSynonymPermission(ctx context.Context, canEdit bool) (
actions []*schema.PermissionMemberAction) {
actions = make([]*schema.PermissionMemberAction, 0)
if canEdit {
actions = append(actions, &schema.PermissionMemberAction{
Action: "edit",
Name: "Edit",
Type: "edit",
})
}
return actions
}

View File

@ -3,6 +3,7 @@ package service
import (
"github.com/answerdev/answer/internal/service/action"
"github.com/answerdev/answer/internal/service/activity"
"github.com/answerdev/answer/internal/service/activity_common"
answercommon "github.com/answerdev/answer/internal/service/answer_common"
"github.com/answerdev/answer/internal/service/auth"
collectioncommon "github.com/answerdev/answer/internal/service/collection_common"
@ -22,9 +23,9 @@ 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/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"
@ -72,4 +73,6 @@ var ProviderSetService = wire.NewSet(
notification.NewNotificationService,
activity.NewAnswerActivityService,
dashboard.NewDashboardService,
activity_common.NewActivityCommon,
activity.NewActivityService,
)

View File

@ -5,8 +5,10 @@ import (
"encoding/json"
"time"
"github.com/answerdev/answer/internal/base/constant"
"github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/internal/service/activity_common"
"github.com/answerdev/answer/internal/service/activity_queue"
"github.com/answerdev/answer/internal/service/config"
"github.com/answerdev/answer/internal/service/meta"
"github.com/segmentfault/pacman/errors"
@ -113,6 +115,12 @@ func (qs *QuestionCommon) UpdataPostTime(ctx context.Context, questionID string)
questioninfo.PostUpdateTime = now
return qs.questionRepo.UpdateQuestion(ctx, questioninfo, []string{"post_update_time"})
}
func (qs *QuestionCommon) UpdataPostSetTime(ctx context.Context, questionID string, setTime time.Time) error {
questioninfo := &entity.Question{}
questioninfo.ID = questionID
questioninfo.PostUpdateTime = setTime
return qs.questionRepo.UpdateQuestion(ctx, questioninfo, []string{"post_update_time"})
}
func (qs *QuestionCommon) FindInfoByID(ctx context.Context, questionIDs []string, loginUserID string) (map[string]*schema.QuestionInfo, error) {
list := make(map[string]*schema.QuestionInfo)
@ -182,14 +190,26 @@ func (qs *QuestionCommon) Info(ctx context.Context, questionID string, loginUser
}
showinfo.Tags = tagmap
userinfo, has, err := qs.userCommon.GetUserBasicInfoByID(ctx, dbinfo.UserID)
userIds := make([]string, 0)
userIds = append(userIds, dbinfo.UserID)
userIds = append(userIds, dbinfo.LastEditUserID)
userIds = append(userIds, showinfo.LastAnsweredUserID)
userInfoMap, err := qs.userCommon.BatchUserBasicInfoByID(ctx, userIds)
if err != nil {
return showinfo, err
}
if has {
showinfo.UserInfo = userinfo
showinfo.UpdateUserInfo = userinfo
showinfo.LastAnsweredUserInfo = userinfo
_, ok := userInfoMap[dbinfo.UserID]
if ok {
showinfo.UserInfo = userInfoMap[dbinfo.UserID]
}
_, ok = userInfoMap[dbinfo.LastEditUserID]
if ok {
showinfo.UpdateUserInfo = userInfoMap[dbinfo.LastEditUserID]
}
_, ok = userInfoMap[showinfo.LastAnsweredUserID]
if ok {
showinfo.LastAnsweredUserInfo = userInfoMap[showinfo.LastAnsweredUserID]
}
if loginUserID == "" {
@ -214,7 +234,7 @@ func (qs *QuestionCommon) Info(ctx context.Context, questionID string, loginUser
if err != nil {
log.Error("CollectionFunc.SearchObjectCollected", err)
}
_, ok := CollectedMap[dbinfo.ID]
_, ok = CollectedMap[dbinfo.ID]
if ok {
showinfo.Collected = true
}
@ -231,7 +251,9 @@ func (qs *QuestionCommon) ListFormat(ctx context.Context, questionList []*entity
item := qs.ShowListFormat(ctx, questionInfo)
list = append(list, item)
objectIds = append(objectIds, item.ID)
userIds = append(userIds, questionInfo.UserID)
userIds = append(userIds, item.UserID)
userIds = append(userIds, item.LastEditUserID)
userIds = append(userIds, item.LastAnsweredUserID)
}
tagsMap, err := qs.tagCommon.BatchGetObjectTag(ctx, objectIds)
if err != nil {
@ -251,8 +273,14 @@ func (qs *QuestionCommon) ListFormat(ctx context.Context, questionList []*entity
_, ok = userInfoMap[item.UserID]
if ok {
item.UserInfo = userInfoMap[item.UserID]
item.UpdateUserInfo = userInfoMap[item.UserID]
item.LastAnsweredUserInfo = userInfoMap[item.UserID]
}
_, ok = userInfoMap[item.LastEditUserID]
if ok {
item.UpdateUserInfo = userInfoMap[item.LastEditUserID]
}
_, ok = userInfoMap[item.LastAnsweredUserID]
if ok {
item.LastAnsweredUserInfo = userInfoMap[item.LastAnsweredUserID]
}
}
@ -308,7 +336,7 @@ func (qs *QuestionCommon) CloseQuestion(ctx context.Context, req *schema.CloseQu
if !has {
return nil
}
questionInfo.Status = entity.QuestionStatusclosed
questionInfo.Status = entity.QuestionStatusClosed
err = qs.questionRepo.UpdateQuestionStatus(ctx, questionInfo)
if err != nil {
return err
@ -322,6 +350,13 @@ func (qs *QuestionCommon) CloseQuestion(ctx context.Context, req *schema.CloseQu
if err != nil {
return err
}
activity_queue.AddActivity(&schema.ActivityMsg{
UserID: questionInfo.UserID,
ObjectID: questionInfo.ID,
OriginalObjectID: questionInfo.ID,
ActivityTypeKey: constant.ActQuestionClosed,
})
return nil
}
@ -371,9 +406,41 @@ func (qs *QuestionCommon) ShowFormat(ctx context.Context, data *entity.Question)
info.CreateTime = data.CreatedAt.Unix()
info.UpdateTime = data.UpdatedAt.Unix()
info.PostUpdateTime = data.PostUpdateTime.Unix()
if data.PostUpdateTime.Unix() < 1 {
info.PostUpdateTime = 0
}
info.QuestionUpdateTime = data.UpdatedAt.Unix()
if data.UpdatedAt.Unix() < 1 {
info.QuestionUpdateTime = 0
}
info.Status = data.Status
info.UserID = data.UserID
info.LastEditUserID = data.LastEditUserID
if data.LastAnswerID != "0" {
answerInfo, exist, err := qs.answerRepo.GetAnswer(ctx, data.LastAnswerID)
if err == nil && exist {
if answerInfo.LastEditUserID != "0" {
info.LastAnsweredUserID = answerInfo.LastEditUserID
} else {
info.LastAnsweredUserID = answerInfo.UserID
}
}
}
info.Tags = make([]*schema.TagResp, 0)
return &info
}
func (qs *QuestionCommon) ShowFormatWithTag(ctx context.Context, data *entity.QuestionWithTagsRevision) *schema.QuestionInfo {
info := qs.ShowFormat(ctx, &data.Question)
Tags := make([]*schema.TagResp, 0)
for _, tag := range data.Tags {
item := &schema.TagResp{}
item.SlugName = tag.SlugName
item.DisplayName = tag.DisplayName
item.Recommend = tag.Recommend
item.Reserved = tag.Reserved
Tags = append(Tags, item)
}
info.Tags = Tags
return info
}

View File

@ -7,11 +7,14 @@ import (
"time"
"github.com/answerdev/answer/internal/base/constant"
"github.com/answerdev/answer/internal/base/handler"
"github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/internal/base/translator"
"github.com/answerdev/answer/internal/base/validator"
"github.com/answerdev/answer/internal/entity"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/activity"
"github.com/answerdev/answer/internal/service/activity_queue"
collectioncommon "github.com/answerdev/answer/internal/service/collection_common"
"github.com/answerdev/answer/internal/service/meta"
"github.com/answerdev/answer/internal/service/notice_queue"
@ -71,7 +74,13 @@ func (qs *QuestionService) CloseQuestion(ctx context.Context, req *schema.CloseQ
if !has {
return nil
}
questionInfo.Status = entity.QuestionStatusclosed
if !req.IsAdmin {
if questionInfo.UserID != req.UserID {
return errors.BadRequest(reason.QuestionCannotClose)
}
}
questionInfo.Status = entity.QuestionStatusClosed
err = qs.questionRepo.UpdateQuestionStatus(ctx, questionInfo)
if err != nil {
return err
@ -85,6 +94,13 @@ func (qs *QuestionService) CloseQuestion(ctx context.Context, req *schema.CloseQ
if err != nil {
return err
}
activity_queue.AddActivity(&schema.ActivityMsg{
UserID: req.UserID,
ObjectID: questionInfo.ID,
OriginalObjectID: questionInfo.ID,
ActivityTypeKey: constant.ActQuestionClosed,
})
return nil
}
@ -105,18 +121,21 @@ 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) {
func (qs *QuestionService) AddQuestion(ctx context.Context, req *schema.QuestionAdd) (questionInfo any, 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
errorlist := make([]*validator.FormErrorField, 0)
errorlist = append(errorlist, &validator.FormErrorField{
ErrorField: "tags",
ErrorMsg: translator.GlobalTrans.Tr(handler.GetLangByCtx(ctx), reason.RecommendTagEnter),
})
err = errors.BadRequest(reason.RecommendTagEnter)
return errorlist, err
}
questionInfo = &schema.QuestionInfo{}
question := &entity.Question{}
now := time.Now()
question.UserID = req.UserID
@ -125,11 +144,12 @@ func (qs *QuestionService) AddQuestion(ctx context.Context, req *schema.Question
question.ParsedText = req.HTML
question.AcceptedAnswerID = "0"
question.LastAnswerID = "0"
question.PostUpdateTime = now
question.LastEditUserID = "0"
//question.PostUpdateTime = nil
question.Status = entity.QuestionStatusAvailable
question.RevisionID = "0"
question.CreatedAt = now
question.UpdatedAt = now
//question.UpdatedAt = nil
err = qs.questionRepo.AddQuestion(ctx, question)
if err != nil {
return
@ -146,11 +166,25 @@ func (qs *QuestionService) AddQuestion(ctx context.Context, req *schema.Question
revisionDTO := &schema.AddRevisionDTO{
UserID: question.UserID,
ObjectID: question.ID,
Title: "",
Title: question.Title,
}
infoJSON, _ := json.Marshal(question)
tagNameList := make([]string, 0)
for _, tag := range req.Tags {
tagNameList = append(tagNameList, tag.SlugName)
}
Tags, tagerr := qs.tagCommon.GetTagListByNames(ctx, tagNameList)
if tagerr != nil {
return questionInfo, tagerr
}
questionWithTagsRevision, err := qs.changeQuestionToRevision(ctx, question, Tags)
if err != nil {
return nil, err
}
infoJSON, _ := json.Marshal(questionWithTagsRevision)
revisionDTO.Content = string(infoJSON)
err = qs.revisionService.AddRevision(ctx, revisionDTO, true)
revisionID, err := qs.revisionService.AddRevision(ctx, revisionDTO, true)
if err != nil {
return
}
@ -161,7 +195,15 @@ func (qs *QuestionService) AddQuestion(ctx context.Context, req *schema.Question
log.Error("user IncreaseQuestionCount error", err.Error())
}
questionInfo, err = qs.GetQuestion(ctx, question.ID, question.UserID, false)
activity_queue.AddActivity(&schema.ActivityMsg{
UserID: question.UserID,
ObjectID: question.ID,
OriginalObjectID: question.ID,
ActivityTypeKey: constant.ActQuestionAsked,
RevisionID: revisionID,
})
questionInfo, err = qs.GetQuestion(ctx, question.ID, question.UserID, req.QuestionPermission)
return
}
@ -174,15 +216,31 @@ func (qs *QuestionService) RemoveQuestion(ctx context.Context, req *schema.Remov
if !has {
return nil
}
if questionInfo.UserID != req.UserID {
return errors.BadRequest(reason.UnauthorizedError)
}
if !req.IsAdmin {
if questionInfo.UserID != req.UserID {
return errors.BadRequest(reason.QuestionCannotDeleted)
}
if questionInfo.AcceptedAnswerID != "" {
return errors.BadRequest(reason.UnauthorizedError)
}
if questionInfo.AnswerCount > 0 {
return errors.BadRequest(reason.UnauthorizedError)
if questionInfo.AcceptedAnswerID != "0" {
return errors.BadRequest(reason.QuestionCannotDeleted)
}
if questionInfo.AnswerCount > 1 {
return errors.BadRequest(reason.QuestionCannotDeleted)
}
if questionInfo.AnswerCount == 1 {
answersearch := &entity.AnswerSearch{}
answersearch.QuestionID = req.ID
answerList, _, err := qs.questioncommon.AnswerCommon.Search(ctx, answersearch)
if err != nil {
return err
}
for _, answer := range answerList {
if answer.VoteCount > 0 {
return errors.BadRequest(reason.QuestionCannotDeleted)
}
}
}
}
questionInfo.Status = entity.QuestionStatusDeleted
@ -201,106 +259,198 @@ func (qs *QuestionService) RemoveQuestion(ctx context.Context, req *schema.Remov
if err != nil {
log.Errorf("user DeleteQuestion rank rollback error %s", err.Error())
}
activity_queue.AddActivity(&schema.ActivityMsg{
UserID: questionInfo.UserID,
ObjectID: questionInfo.ID,
OriginalObjectID: questionInfo.ID,
ActivityTypeKey: constant.ActQuestionDeleted,
})
return nil
}
// UpdateQuestion update question
func (qs *QuestionService) UpdateQuestion(ctx context.Context, req *schema.QuestionUpdate) (questionInfo *schema.QuestionInfo, err error) {
func (qs *QuestionService) UpdateQuestion(ctx context.Context, req *schema.QuestionUpdate) (questionInfo any, err error) {
var canUpdate bool
questionInfo = &schema.QuestionInfo{}
now := time.Now()
question := &entity.Question{}
question.UserID = req.UserID
question.Title = req.Title
question.OriginalText = req.Content
question.ParsedText = req.HTML
question.ID = req.ID
question.UpdatedAt = now
dbinfo, has, err := qs.questionRepo.GetQuestion(ctx, question.ID)
_, existUnreviewed, err := qs.revisionService.ExistUnreviewedByObjectID(ctx, req.ID)
if err != nil {
return
}
if existUnreviewed {
err = errors.BadRequest(reason.QuestionCannotUpdate)
return
}
dbinfo, has, err := qs.questionRepo.GetQuestion(ctx, req.ID)
if err != nil {
return
}
if !has {
return
}
now := time.Now()
question := &entity.Question{}
question.Title = req.Title
question.OriginalText = req.Content
question.ParsedText = req.HTML
question.ID = req.ID
question.UpdatedAt = now
question.PostUpdateTime = now
question.UserID = dbinfo.UserID
question.LastEditUserID = "0"
if dbinfo.UserID != req.UserID {
return
question.LastEditUserID = req.UserID
}
//CheckChangeTag
oldTags, err := qs.tagCommon.GetObjectEntityTag(ctx, question.ID)
if err != nil {
return
oldTags, tagerr := qs.tagCommon.GetObjectEntityTag(ctx, question.ID)
if tagerr != nil {
return questionInfo, tagerr
}
tagNameList := make([]string, 0)
oldtagNameList := make([]string, 0)
for _, tag := range req.Tags {
tagNameList = append(tagNameList, tag.SlugName)
}
Tags, err := qs.tagCommon.GetTagListByNames(ctx, tagNameList)
if err != nil {
return
for _, tag := range oldTags {
oldtagNameList = append(oldtagNameList, tag.SlugName)
}
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, ",")))
isChange := qs.tagCommon.CheckTagsIsChange(ctx, tagNameList, oldtagNameList)
//If the content is the same, ignore it
if dbinfo.Title == req.Title && dbinfo.OriginalText == req.Content && !isChange {
return
}
//update question to db
err = qs.questionRepo.UpdateQuestion(ctx, question, []string{"title", "original_text", "parsed_text", "updated_at"})
Tags, tagerr := qs.tagCommon.GetTagListByNames(ctx, tagNameList)
if tagerr != nil {
return questionInfo, tagerr
}
// If it's not admin
if !req.IsAdmin {
//CheckChangeTag
CheckTag, CheckTaglist := qs.CheckChangeReservedTag(ctx, oldTags, Tags)
if !CheckTag {
errMsg := fmt.Sprintf(`The reserved tag "%s" must be present.`,
strings.Join(CheckTaglist, ","))
errorlist := make([]*validator.FormErrorField, 0)
errorlist = append(errorlist, &validator.FormErrorField{
ErrorField: "tags",
ErrorMsg: errMsg,
})
err = errors.BadRequest(reason.RequestFormatError).WithMsg(errMsg)
return errorlist, err
}
}
// Check whether mandatory labels are selected
recommendExist, err := qs.tagCommon.ExistRecommend(ctx, req.Tags)
if err != nil {
return
}
objectTagData := schema.TagChange{}
objectTagData.ObjectID = question.ID
objectTagData.Tags = req.Tags
objectTagData.UserID = req.UserID
err = qs.ChangeTag(ctx, &objectTagData)
if err != nil {
return
if !recommendExist {
errorlist := make([]*validator.FormErrorField, 0)
errorlist = append(errorlist, &validator.FormErrorField{
ErrorField: "tags",
ErrorMsg: translator.GlobalTrans.Tr(handler.GetLangByCtx(ctx), reason.RecommendTagEnter),
})
err = errors.BadRequest(reason.RecommendTagEnter)
return errorlist, err
}
//Administrators and themselves do not need to be audited
revisionDTO := &schema.AddRevisionDTO{
UserID: question.UserID,
ObjectID: question.ID,
Title: "",
Title: question.Title,
Log: req.EditSummary,
}
infoJSON, _ := json.Marshal(question)
if req.NoNeedReview || req.IsAdmin || dbinfo.UserID == req.UserID {
canUpdate = true
}
// It's not you or the administrator that needs to be reviewed
if !canUpdate {
revisionDTO.Status = entity.RevisionUnreviewedStatus
} else {
//Direct modification
revisionDTO.Status = entity.RevisionReviewPassStatus
//update question to db
saveerr := qs.questionRepo.UpdateQuestion(ctx, question, []string{"title", "original_text", "parsed_text", "updated_at", "post_update_time", "last_edit_user_id"})
if saveerr != nil {
return questionInfo, saveerr
}
objectTagData := schema.TagChange{}
objectTagData.ObjectID = question.ID
objectTagData.Tags = req.Tags
objectTagData.UserID = req.UserID
tagerr := qs.ChangeTag(ctx, &objectTagData)
if err != nil {
return questionInfo, tagerr
}
}
questionWithTagsRevision, err := qs.changeQuestionToRevision(ctx, question, Tags)
if err != nil {
return nil, err
}
infoJSON, _ := json.Marshal(questionWithTagsRevision)
revisionDTO.Content = string(infoJSON)
err = qs.revisionService.AddRevision(ctx, revisionDTO, true)
revisionID, err := qs.revisionService.AddRevision(ctx, revisionDTO, true)
if err != nil {
return
}
if canUpdate {
activity_queue.AddActivity(&schema.ActivityMsg{
UserID: req.UserID,
ObjectID: question.ID,
ActivityTypeKey: constant.ActQuestionEdited,
RevisionID: revisionID,
OriginalObjectID: question.ID,
})
}
questionInfo, err = qs.GetQuestion(ctx, question.ID, question.UserID, false)
questionInfo, err = qs.GetQuestion(ctx, question.ID, question.UserID, req.QuestionPermission)
return
}
// GetQuestion get question one
func (qs *QuestionService) GetQuestion(ctx context.Context, id, loginUserID string, addpv bool) (resp *schema.QuestionInfo, err error) {
question, err := qs.questioncommon.Info(ctx, id, loginUserID)
func (qs *QuestionService) GetQuestion(ctx context.Context, questionID, userID string,
per schema.QuestionPermission) (resp *schema.QuestionInfo, err error) {
question, err := qs.questioncommon.Info(ctx, questionID, userID)
if err != nil {
return
}
if addpv {
err = qs.questioncommon.UpdataPv(ctx, id)
if err != nil {
log.Error("UpdataPv", err)
}
}
question.MemberActions = permission.GetQuestionPermission(loginUserID, question.UserID)
question.MemberActions = permission.GetQuestionPermission(ctx, userID, question.UserID,
per.CanEdit, per.CanDelete, per.CanClose)
return question, nil
}
// GetQuestionAndAddPV get question one
func (qs *QuestionService) GetQuestionAndAddPV(ctx context.Context, questionID, loginUserID string,
per schema.QuestionPermission) (
resp *schema.QuestionInfo, err error) {
err = qs.questioncommon.UpdataPv(ctx, questionID)
if err != nil {
log.Error(err)
}
return qs.GetQuestion(ctx, questionID, loginUserID, per)
}
func (qs *QuestionService) ChangeTag(ctx context.Context, objectTagData *schema.TagChange) error {
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) CheckChangeReservedTag(ctx context.Context, oldobjectTagData, objectTagData []*entity.Tag) (bool, []string) {
return qs.tagCommon.CheckChangeReservedTag(ctx, oldobjectTagData, objectTagData)
}
func (qs *QuestionService) SearchUserList(ctx context.Context, userName, order string, page, pageSize int, loginUserID string) ([]*schema.UserQuestionInfo, int64, error) {
@ -518,12 +668,12 @@ func (qs *QuestionService) SearchByTitleLike(ctx context.Context, title string,
// SimilarQuestion
func (qs *QuestionService) SimilarQuestion(ctx context.Context, questionID string, loginUserID string) ([]*schema.QuestionInfo, int64, error) {
list := make([]*schema.QuestionInfo, 0)
questionInfo, err := qs.GetQuestion(ctx, questionID, loginUserID, false)
question, err := qs.questioncommon.Info(ctx, questionID, loginUserID)
if err != nil {
return list, 0, err
return list, 0, nil
}
tagNames := make([]string, 0, len(questionInfo.Tags))
for _, tag := range questionInfo.Tags {
tagNames := make([]string, 0, len(question.Tags))
for _, tag := range question.Tags {
tagNames = append(tagNames, tag.SlugName)
}
search := &schema.QuestionSearch{}
@ -581,8 +731,7 @@ func (qs *QuestionService) AdminSetQuestionStatus(ctx context.Context, questionI
if !exist {
return errors.BadRequest(reason.QuestionNotFound)
}
questionInfo.Status = setStatus
err = qs.questionRepo.UpdateQuestionStatus(ctx, questionInfo)
err = qs.questionRepo.UpdateQuestionStatus(ctx, &entity.Question{ID: questionInfo.ID, Status: setStatus})
if err != nil {
return err
}
@ -592,6 +741,28 @@ func (qs *QuestionService) AdminSetQuestionStatus(ctx context.Context, questionI
if err != nil {
log.Errorf("admin delete question then rank rollback error %s", err.Error())
}
activity_queue.AddActivity(&schema.ActivityMsg{
UserID: questionInfo.UserID,
ObjectID: questionInfo.ID,
OriginalObjectID: questionInfo.ID,
ActivityTypeKey: constant.ActQuestionDeleted,
})
}
if setStatus == entity.QuestionStatusAvailable && questionInfo.Status == entity.QuestionStatusClosed {
activity_queue.AddActivity(&schema.ActivityMsg{
UserID: questionInfo.UserID,
ObjectID: questionInfo.ID,
OriginalObjectID: questionInfo.ID,
ActivityTypeKey: constant.ActQuestionReopened,
})
}
if setStatus == entity.QuestionStatusClosed && questionInfo.Status != entity.QuestionStatusClosed {
activity_queue.AddActivity(&schema.ActivityMsg{
UserID: questionInfo.UserID,
ObjectID: questionInfo.ID,
OriginalObjectID: questionInfo.ID,
ActivityTypeKey: constant.ActQuestionClosed,
})
}
msg := &schema.NotificationMsg{}
msg.ObjectID = questionInfo.ID
@ -688,3 +859,16 @@ func (qs *QuestionService) CmsSearchAnswerList(ctx context.Context, search *enti
}
return answerlist, count, nil
}
func (qs *QuestionService) changeQuestionToRevision(ctx context.Context, questionInfo *entity.Question, tags []*entity.Tag) (
questionRevision *entity.QuestionWithTagsRevision, err error) {
questionRevision = &entity.QuestionWithTagsRevision{}
questionRevision.Question = *questionInfo
for _, tag := range tags {
item := &entity.TagSimpleInfoForRevision{}
_ = copier.Copy(item, tag)
questionRevision.Tags = append(questionRevision.Tags, item)
}
return questionRevision, nil
}

View File

@ -3,6 +3,7 @@ package rank
import (
"context"
"github.com/answerdev/answer/internal/base/constant"
"github.com/answerdev/answer/internal/base/pager"
"github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/internal/entity"
@ -17,27 +18,35 @@ import (
)
const (
QuestionAddRank = "rank.question.add"
QuestionEditRank = "rank.question.edit"
QuestionDeleteRank = "rank.question.delete"
QuestionVoteUpRank = "rank.question.vote_up"
QuestionVoteDownRank = "rank.question.vote_down"
AnswerAddRank = "rank.answer.add"
AnswerEditRank = "rank.answer.edit"
AnswerDeleteRank = "rank.answer.delete"
AnswerAcceptRank = "rank.answer.accept"
AnswerVoteUpRank = "rank.answer.vote_up"
AnswerVoteDownRank = "rank.answer.vote_down"
CommentAddRank = "rank.comment.add"
CommentEditRank = "rank.comment.edit"
CommentDeleteRank = "rank.comment.delete"
ReportAddRank = "rank.report.add"
TagAddRank = "rank.tag.add"
TagEditRank = "rank.tag.edit"
TagDeleteRank = "rank.tag.delete"
TagSynonymRank = "rank.tag.synonym"
LinkUrlLimitRank = "rank.link.url_limit"
VoteDetailRank = "rank.vote.detail"
QuestionAddRank = "rank.question.add"
QuestionEditRank = "rank.question.edit"
QuestionEditWithoutReviewRank = "rank.question.edit_without_review"
QuestionDeleteRank = "rank.question.delete"
QuestionVoteUpRank = "rank.question.vote_up"
QuestionVoteDownRank = "rank.question.vote_down"
AnswerAddRank = "rank.answer.add"
AnswerEditRank = "rank.answer.edit"
AnswerEditWithoutReviewRank = "rank.answer.edit_without_review"
AnswerDeleteRank = "rank.answer.delete"
AnswerAcceptRank = "rank.answer.accept"
AnswerVoteUpRank = "rank.answer.vote_up"
AnswerVoteDownRank = "rank.answer.vote_down"
CommentAddRank = "rank.comment.add"
CommentEditRank = "rank.comment.edit"
CommentDeleteRank = "rank.comment.delete"
CommentVoteUpRank = "rank.comment.vote_up"
CommentVoteDownRank = "rank.comment.vote_down"
ReportAddRank = "rank.report.add"
TagAddRank = "rank.tag.add"
TagEditRank = "rank.tag.edit"
TagEditWithoutReviewRank = "rank.tag.edit_without_review"
TagDeleteRank = "rank.tag.delete"
TagSynonymRank = "rank.tag.synonym"
LinkUrlLimitRank = "rank.link.url_limit"
VoteDetailRank = "rank.vote.detail"
AnswerAuditRank = "rank.answer.audit"
QuestionAuditRank = "rank.question.audit"
TagAuditRank = "rank.tag.audit"
)
type UserRankRepo interface {
@ -67,8 +76,9 @@ func NewRankService(
}
}
// CheckRankPermission check whether the user reputation meets the permission
func (rs *RankService) CheckRankPermission(ctx context.Context, userID string, action string) (can bool, err error) {
// CheckOperationPermission verify that the user has permission
func (rs *RankService) CheckOperationPermission(ctx context.Context, userID string, action string, objectID string) (
can bool, err error) {
if len(userID) == 0 {
return false, nil
}
@ -81,17 +91,134 @@ func (rs *RankService) CheckRankPermission(ctx context.Context, userID string, a
if !exist {
return false, nil
}
currentUserRank := userInfo.Rank
// administrator have all permissions
if userInfo.IsAdmin {
return true, nil
}
if len(objectID) > 0 {
objectInfo, err := rs.objectInfoService.GetInfo(ctx, objectID)
if err != nil {
return can, err
}
// if the user is this object creator, the user can operate this object.
if objectInfo != nil &&
objectInfo.ObjectCreatorUserID == userID {
return true, nil
}
}
return rs.checkUserRank(ctx, userInfo.ID, userInfo.Rank, action)
}
// CheckOperationPermissions verify that the user has permission
func (rs *RankService) CheckOperationPermissions(ctx context.Context, userID string, actions []string, objectID string) (
can []bool, err error) {
can = make([]bool, len(actions))
if len(userID) == 0 {
return can, nil
}
// get the rank of the current user
userInfo, exist, err := rs.userCommon.GetUserBasicInfoByID(ctx, userID)
if err != nil {
return can, err
}
if !exist {
return can, nil
}
objectOwner := false
if len(objectID) > 0 {
objectInfo, err := rs.objectInfoService.GetInfo(ctx, objectID)
if err != nil {
return can, err
}
// if the user is this object creator, the user can operate this object.
if objectInfo != nil &&
objectInfo.ObjectCreatorUserID == userID {
objectOwner = true
}
}
for idx, action := range actions {
if userInfo.IsAdmin || objectOwner {
can[idx] = true
continue
}
meetRank, err := rs.checkUserRank(ctx, userInfo.ID, userInfo.Rank, action)
if err != nil {
log.Error(err)
}
can[idx] = meetRank
}
return can, nil
}
// CheckVotePermission verify that the user has vote permission
func (rs *RankService) CheckVotePermission(ctx context.Context, userID, objectID string, voteUp bool) (
can bool, err error) {
if len(userID) == 0 || len(objectID) == 0 {
return false, nil
}
// get the rank of the current user
userInfo, exist, err := rs.userCommon.GetUserBasicInfoByID(ctx, userID)
if err != nil {
return can, err
}
if !exist {
return can, nil
}
// administrator have all permissions
if userInfo.IsAdmin {
return true, nil
}
objectInfo, err := rs.objectInfoService.GetInfo(ctx, objectID)
if err != nil {
return can, err
}
action := ""
switch objectInfo.ObjectType {
case constant.QuestionObjectType:
if voteUp {
action = QuestionVoteUpRank
} else {
action = QuestionVoteDownRank
}
case constant.AnswerObjectType:
if voteUp {
action = AnswerVoteUpRank
} else {
action = AnswerVoteDownRank
}
case constant.CommentObjectType:
if voteUp {
action = CommentVoteUpRank
} else {
action = CommentVoteDownRank
}
}
meetRank, err := rs.checkUserRank(ctx, userInfo.ID, userInfo.Rank, action)
if err != nil {
log.Error(err)
}
return meetRank, nil
}
// CheckRankPermission verify that the user meets the prestige criteria
func (rs *RankService) checkUserRank(ctx context.Context, userID string, userRank int, action string) (
can bool, err error) {
// get the amount of rank required for the current operation
requireRank, err := rs.configRepo.GetInt(action)
if err != nil {
return false, err
}
if currentUserRank < requireRank {
if userRank < requireRank || requireRank < 0 {
log.Debugf("user %s want to do action %s, but rank %d < %d",
userInfo.DisplayName, action, currentUserRank, requireRank)
userID, action, userRank, requireRank)
return false, nil
}
return true, nil

View File

@ -47,7 +47,7 @@ func (rs *ReportService) AddReport(ctx context.Context, req *schema.AddReportReq
report := &entity.Report{
UserID: req.UserID,
ReportedUserID: objInfo.ObjectCreator,
ReportedUserID: objInfo.ObjectCreatorUserID,
ObjectID: req.ObjectID,
ObjectType: objectTypeNumber,
ReportType: req.ReportType,

View File

@ -2,6 +2,7 @@ package revision
import (
"context"
"github.com/answerdev/answer/internal/entity"
"xorm.io/xorm"
)
@ -9,7 +10,11 @@ import (
// RevisionRepo revision repository
type RevisionRepo interface {
AddRevision(ctx context.Context, revision *entity.Revision, autoUpdateRevisionID bool) (err error)
GetRevisionByID(ctx context.Context, revisionID string) (revision *entity.Revision, exist bool, err error)
GetLastRevisionByObjectID(ctx context.Context, objectID string) (revision *entity.Revision, exist bool, err error)
GetRevisionList(ctx context.Context, revision *entity.Revision) (revisionList []entity.Revision, err error)
UpdateObjectRevisionId(ctx context.Context, revision *entity.Revision, session *xorm.Session) (err error)
ExistUnreviewedByObjectID(ctx context.Context, objectID string) (revision *entity.Revision, exist bool, err error)
GetUnreviewedRevisionPage(ctx context.Context, page, pageSize int, objectTypes []int) ([]*entity.Revision, int64, error)
UpdateStatus(ctx context.Context, id string, status int, reviewUserID string) (err error)
}

View File

@ -3,8 +3,11 @@ package revision_common
import (
"context"
"github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/internal/service/revision"
usercommon "github.com/answerdev/answer/internal/service/user_common"
"github.com/segmentfault/pacman/errors"
"github.com/segmentfault/pacman/log"
"github.com/answerdev/answer/internal/entity"
"github.com/answerdev/answer/internal/schema"
@ -24,17 +27,46 @@ func NewRevisionService(revisionRepo revision.RevisionRepo, userRepo usercommon.
}
}
func (rs *RevisionService) GetUnreviewedRevisionCount(ctx context.Context, req *schema.RevisionSearch) (count int64, err error) {
if len(req.GetCanReviewObjectTypes()) == 0 {
return 0, nil
}
_, count, err = rs.revisionRepo.GetUnreviewedRevisionPage(ctx, req.Page, 1, req.GetCanReviewObjectTypes())
return count, err
}
// AddRevision add revision
//
// autoUpdateRevisionID bool : if autoUpdateRevisionID is true , the object.revision_id will be updated,
// if not need auto update object.revision_id, it must be false.
// example: user can edit the object, but need audit, the revision_id will be updated when admin approved
func (rs *RevisionService) AddRevision(ctx context.Context, req *schema.AddRevisionDTO, autoUpdateRevisionID bool) (err error) {
func (rs *RevisionService) AddRevision(ctx context.Context, req *schema.AddRevisionDTO, autoUpdateRevisionID bool) (
revisionID string, err error) {
rev := &entity.Revision{}
_ = copier.Copy(rev, req)
err = rs.revisionRepo.AddRevision(ctx, rev, autoUpdateRevisionID)
if err != nil {
return err
return "", err
}
return nil
return rev.ID, nil
}
// GetRevision get revision
func (rs *RevisionService) GetRevision(ctx context.Context, revisionID string) (
revision *entity.Revision, err error) {
revisionInfo, exist, err := rs.revisionRepo.GetRevisionByID(ctx, revisionID)
if err != nil {
log.Error(err)
return nil, err
}
if !exist {
return nil, errors.BadRequest(reason.ObjectNotFound)
}
return revisionInfo, nil
}
// ExistUnreviewedByObjectID
func (rs *RevisionService) ExistUnreviewedByObjectID(ctx context.Context, objectID string) (revision *entity.Revision, exist bool, err error) {
revision, exist, err = rs.revisionRepo.ExistUnreviewedByObjectID(ctx, objectID)
return revision, exist, err
}

View File

@ -3,37 +3,316 @@ package service
import (
"context"
"encoding/json"
"time"
"github.com/answerdev/answer/internal/base/constant"
"github.com/answerdev/answer/internal/base/pager"
"github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/internal/entity"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/activity_queue"
answercommon "github.com/answerdev/answer/internal/service/answer_common"
"github.com/answerdev/answer/internal/service/notice_queue"
"github.com/answerdev/answer/internal/service/object_info"
questioncommon "github.com/answerdev/answer/internal/service/question_common"
"github.com/answerdev/answer/internal/service/revision"
"github.com/answerdev/answer/internal/service/tag_common"
tagcommon "github.com/answerdev/answer/internal/service/tag_common"
usercommon "github.com/answerdev/answer/internal/service/user_common"
"github.com/answerdev/answer/pkg/converter"
"github.com/answerdev/answer/pkg/obj"
"github.com/jinzhu/copier"
"github.com/segmentfault/pacman/errors"
"github.com/segmentfault/pacman/log"
)
// RevisionService user service
type RevisionService struct {
revisionRepo revision.RevisionRepo
userCommon *usercommon.UserCommon
questionCommon *questioncommon.QuestionCommon
answerService *AnswerService
revisionRepo revision.RevisionRepo
userCommon *usercommon.UserCommon
questionCommon *questioncommon.QuestionCommon
answerService *AnswerService
objectInfoService *object_info.ObjService
questionRepo questioncommon.QuestionRepo
answerRepo answercommon.AnswerRepo
tagRepo tag_common.TagRepo
tagCommon *tagcommon.TagCommonService
}
func NewRevisionService(
revisionRepo revision.RevisionRepo,
userCommon *usercommon.UserCommon,
questionCommon *questioncommon.QuestionCommon,
answerService *AnswerService) *RevisionService {
answerService *AnswerService,
objectInfoService *object_info.ObjService,
questionRepo questioncommon.QuestionRepo,
answerRepo answercommon.AnswerRepo,
tagRepo tag_common.TagRepo,
tagCommon *tagcommon.TagCommonService,
) *RevisionService {
return &RevisionService{
revisionRepo: revisionRepo,
userCommon: userCommon,
questionCommon: questionCommon,
answerService: answerService,
revisionRepo: revisionRepo,
userCommon: userCommon,
questionCommon: questionCommon,
answerService: answerService,
objectInfoService: objectInfoService,
questionRepo: questionRepo,
answerRepo: answerRepo,
tagRepo: tagRepo,
tagCommon: tagCommon,
}
}
func (rs *RevisionService) RevisionAudit(ctx context.Context, req *schema.RevisionAuditReq) (err error) {
revisioninfo, exist, err := rs.revisionRepo.GetRevisionByID(ctx, req.ID)
if err != nil {
return
}
if !exist {
return
}
if revisioninfo.Status != entity.RevisionUnreviewedStatus {
return
}
if req.Operation == schema.RevisionAuditReject {
err = rs.revisionRepo.UpdateStatus(ctx, req.ID, entity.RevisionReviewRejectStatus, req.UserID)
return
}
if req.Operation == schema.RevisionAuditApprove {
objectType, objectTypeerr := obj.GetObjectTypeStrByObjectID(revisioninfo.ObjectID)
if objectTypeerr != nil {
return objectTypeerr
}
revisionitem := &schema.GetRevisionResp{}
_ = copier.Copy(revisionitem, revisioninfo)
rs.parseItem(ctx, revisionitem)
var saveErr error
switch objectType {
case constant.QuestionObjectType:
if !req.CanReviewQuestion {
saveErr = errors.BadRequest(reason.RevisionNoPermission)
} else {
saveErr = rs.revisionAuditQuestion(ctx, revisionitem)
}
case constant.AnswerObjectType:
if !req.CanReviewAnswer {
saveErr = errors.BadRequest(reason.RevisionNoPermission)
} else {
saveErr = rs.revisionAuditAnswer(ctx, revisionitem)
}
case constant.TagObjectType:
if !req.CanReviewTag {
saveErr = errors.BadRequest(reason.RevisionNoPermission)
} else {
saveErr = rs.revisionAuditTag(ctx, revisionitem)
}
}
if saveErr != nil {
return saveErr
}
err = rs.revisionRepo.UpdateStatus(ctx, req.ID, entity.RevisionReviewPassStatus, req.UserID)
return
}
return nil
}
func (rs *RevisionService) revisionAuditQuestion(ctx context.Context, revisionitem *schema.GetRevisionResp) (err error) {
questioninfo, ok := revisionitem.ContentParsed.(*schema.QuestionInfo)
if ok {
var PostUpdateTime time.Time
dbquestion, exist, dberr := rs.questionRepo.GetQuestion(ctx, questioninfo.ID)
if dberr != nil || !exist {
return
}
PostUpdateTime = time.Unix(questioninfo.UpdateTime, 0)
if dbquestion.PostUpdateTime.Unix() > PostUpdateTime.Unix() {
PostUpdateTime = dbquestion.PostUpdateTime
}
question := &entity.Question{}
question.ID = questioninfo.ID
question.Title = questioninfo.Title
question.OriginalText = questioninfo.Content
question.ParsedText = questioninfo.HTML
question.UpdatedAt = time.Unix(questioninfo.UpdateTime, 0)
question.PostUpdateTime = PostUpdateTime
question.LastEditUserID = revisionitem.UserID
saveerr := rs.questionRepo.UpdateQuestion(ctx, question, []string{"title", "original_text", "parsed_text", "updated_at", "post_update_time", "last_edit_user_id"})
if saveerr != nil {
return saveerr
}
objectTagTags := make([]*schema.TagItem, 0)
for _, tag := range questioninfo.Tags {
item := &schema.TagItem{}
item.SlugName = tag.SlugName
objectTagTags = append(objectTagTags, item)
}
objectTagData := schema.TagChange{}
objectTagData.ObjectID = question.ID
objectTagData.Tags = objectTagTags
saveerr = rs.tagCommon.ObjectChangeTag(ctx, &objectTagData)
if saveerr != nil {
return saveerr
}
activity_queue.AddActivity(&schema.ActivityMsg{
UserID: revisionitem.UserID,
ObjectID: revisionitem.ObjectID,
ActivityTypeKey: constant.ActQuestionEdited,
RevisionID: revisionitem.ID,
OriginalObjectID: revisionitem.ObjectID,
})
}
return nil
}
func (rs *RevisionService) revisionAuditAnswer(ctx context.Context, revisionitem *schema.GetRevisionResp) (err error) {
answerinfo, ok := revisionitem.ContentParsed.(*schema.AnswerInfo)
if ok {
var PostUpdateTime time.Time
dbquestion, exist, dberr := rs.questionRepo.GetQuestion(ctx, answerinfo.QuestionID)
if dberr != nil || !exist {
return
}
PostUpdateTime = time.Unix(answerinfo.UpdateTime, 0)
if dbquestion.PostUpdateTime.Unix() > PostUpdateTime.Unix() {
PostUpdateTime = dbquestion.PostUpdateTime
}
insertData := new(entity.Answer)
insertData.ID = answerinfo.ID
insertData.OriginalText = answerinfo.Content
insertData.ParsedText = answerinfo.HTML
insertData.UpdatedAt = time.Unix(answerinfo.UpdateTime, 0)
insertData.LastEditUserID = revisionitem.UserID
saveerr := rs.answerRepo.UpdateAnswer(ctx, insertData, []string{"original_text", "parsed_text", "updated_at", "last_edit_user_id"})
if saveerr != nil {
return saveerr
}
saveerr = rs.questionCommon.UpdataPostSetTime(ctx, answerinfo.QuestionID, PostUpdateTime)
if saveerr != nil {
return saveerr
}
questionInfo, exist, err := rs.questionRepo.GetQuestion(ctx, answerinfo.QuestionID)
if err != nil {
return err
}
if !exist {
return errors.BadRequest(reason.QuestionNotFound)
}
msg := &schema.NotificationMsg{
TriggerUserID: revisionitem.UserID,
ReceiverUserID: questionInfo.UserID,
Type: schema.NotificationTypeInbox,
ObjectID: answerinfo.ID,
}
msg.ObjectType = constant.AnswerObjectType
msg.NotificationAction = constant.UpdateAnswer
notice_queue.AddNotification(msg)
activity_queue.AddActivity(&schema.ActivityMsg{
UserID: revisionitem.UserID,
ObjectID: insertData.ID,
OriginalObjectID: insertData.ID,
ActivityTypeKey: constant.ActAnswerEdited,
RevisionID: revisionitem.ID,
})
}
return nil
}
func (rs *RevisionService) revisionAuditTag(ctx context.Context, revisionitem *schema.GetRevisionResp) (err error) {
taginfo, ok := revisionitem.ContentParsed.(*schema.GetTagResp)
if ok {
tag := &entity.Tag{}
tag.ID = taginfo.TagID
tag.OriginalText = taginfo.OriginalText
tag.ParsedText = taginfo.ParsedText
saveerr := rs.tagRepo.UpdateTag(ctx, tag)
if saveerr != nil {
return saveerr
}
tagInfo, exist, err := rs.tagCommon.GetTagByID(ctx, taginfo.TagID)
if err != nil {
return err
}
if !exist {
return errors.BadRequest(reason.TagNotFound)
}
if tagInfo.MainTagID == 0 && len(tagInfo.SlugName) > 0 {
log.Debugf("tag %s update slug_name", tagInfo.SlugName)
tagList, err := rs.tagRepo.GetTagList(ctx, &entity.Tag{MainTagID: converter.StringToInt64(tagInfo.ID)})
if err != nil {
return err
}
updateTagSlugNames := make([]string, 0)
for _, tag := range tagList {
updateTagSlugNames = append(updateTagSlugNames, tag.SlugName)
}
err = rs.tagRepo.UpdateTagSynonym(ctx, updateTagSlugNames, converter.StringToInt64(tagInfo.ID), tagInfo.MainTagSlugName)
if err != nil {
return err
}
}
activity_queue.AddActivity(&schema.ActivityMsg{
UserID: revisionitem.UserID,
ObjectID: taginfo.TagID,
OriginalObjectID: taginfo.TagID,
ActivityTypeKey: constant.ActTagEdited,
RevisionID: revisionitem.ID,
})
}
return nil
}
// GetUnreviewedRevisionPage get unreviewed list
func (rs *RevisionService) GetUnreviewedRevisionPage(ctx context.Context, req *schema.RevisionSearch) (
resp *pager.PageModel, err error) {
revisionResp := make([]*schema.GetUnreviewedRevisionResp, 0)
if len(req.GetCanReviewObjectTypes()) == 0 {
return pager.NewPageModel(0, revisionResp), nil
}
revisionPage, total, err := rs.revisionRepo.GetUnreviewedRevisionPage(
ctx, req.Page, 1, req.GetCanReviewObjectTypes())
if err != nil {
return nil, err
}
for _, rev := range revisionPage {
item := &schema.GetUnreviewedRevisionResp{}
_, ok := constant.ObjectTypeNumberMapping[rev.ObjectType]
if !ok {
continue
}
item.Type = constant.ObjectTypeNumberMapping[rev.ObjectType]
info, err := rs.objectInfoService.GetUnreviewedRevisionInfo(ctx, rev.ObjectID)
if err != nil {
return nil, err
}
item.Info = info
revisionitem := &schema.GetRevisionResp{}
_ = copier.Copy(revisionitem, rev)
rs.parseItem(ctx, revisionitem)
item.UnreviewedInfo = revisionitem
// get user info
userInfo, exists, e := rs.userCommon.GetUserBasicInfoByID(ctx, revisionitem.UserID)
if e != nil {
return nil, e
}
if exists {
var uinfo schema.UserBasicInfo
err = copier.Copy(&uinfo, userInfo)
item.UnreviewedInfo.UserInfo = uinfo
}
revisionResp = append(revisionResp, item)
}
return pager.NewPageModel(total, revisionResp), nil
}
// GetRevisionList get revision list all
func (rs *RevisionService) GetRevisionList(ctx context.Context, req *schema.GetRevisionListReq) (resp []schema.GetRevisionResp, err error) {
var (
@ -75,7 +354,7 @@ func (rs *RevisionService) GetRevisionList(ctx context.Context, req *schema.GetR
func (rs *RevisionService) parseItem(ctx context.Context, item *schema.GetRevisionResp) {
var (
err error
question entity.Question
question entity.QuestionWithTagsRevision
questionInfo *schema.QuestionInfo
answer entity.Answer
answerInfo *schema.AnswerInfo
@ -89,7 +368,7 @@ func (rs *RevisionService) parseItem(ctx context.Context, item *schema.GetRevisi
if err != nil {
break
}
questionInfo = rs.questionCommon.ShowFormat(ctx, &question)
questionInfo = rs.questionCommon.ShowFormatWithTag(ctx, &question)
item.ContentParsed = questionInfo
case constant.ObjectTypeStrMapping["answer"]:
err = json.Unmarshal([]byte(item.Content), &answer)
@ -125,3 +404,16 @@ func (rs *RevisionService) parseItem(ctx context.Context, item *schema.GetRevisi
}
item.CreatedAtParsed = item.CreatedAt.Unix()
}
// CheckCanUpdateRevision can check revision
func (rs *RevisionService) CheckCanUpdateRevision(ctx context.Context, req *schema.CheckCanQuestionUpdate) (
resp *schema.ErrTypeData, err error) {
_, exist, err := rs.revisionRepo.ExistUnreviewedByObjectID(ctx, req.ID)
if err != nil {
return nil, nil
}
if exist {
return &schema.ErrTypeToast, errors.BadRequest(reason.RevisionReviewUnderway)
}
return nil, nil
}

View File

@ -1,102 +0,0 @@
package search
import (
"context"
"regexp"
"strings"
"github.com/answerdev/answer/internal/entity"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/activity_common"
"github.com/answerdev/answer/internal/service/search_common"
"github.com/answerdev/answer/internal/service/tag_common"
)
type TagSearch struct {
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,
tagCommonService *tag_common.TagCommonService, followCommon activity_common.FollowRepo) *TagSearch {
return &TagSearch{
repo: repo,
tagCommonService: tagCommonService,
followCommon: followCommon,
}
}
// Parse
// example: "[tag]hello" -> {exp="tag" w="hello"}
func (ts *TagSearch) Parse(dto *schema.SearchDTO) (ok bool) {
exp := ""
w := dto.Query
q := w
p := `(?m)^\[([a-zA-Z0-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
}
w = strings.TrimSpace(w)
ts.exp = exp
ts.w = w
ts.page = dto.Page
ts.size = dto.Size
ts.userID = dto.UserID
ts.order = dto.Order
return ok
}
func (ts *TagSearch) Search(ctx context.Context) (resp []schema.SearchResp, total int64, err error) {
var (
words []string
tag *entity.Tag
exists, followed bool
)
tag, exists, err = ts.tagCommonService.GetTagBySlugName(ctx, ts.exp)
if err != nil {
return
}
if ts.userID != "" {
followed, err = ts.followCommon.IsFollowed(ts.userID, tag.ID)
}
ts.Extra = schema.GetTagPageResp{
TagID: tag.ID,
SlugName: tag.SlugName,
DisplayName: tag.DisplayName,
OriginalText: tag.OriginalText,
ParsedText: tag.ParsedText,
QuestionCount: tag.QuestionCount,
IsFollower: followed,
Recommend: tag.Recommend,
Reserved: tag.Reserved,
}
ts.Extra.GetExcerpt()
if !exists {
return
}
words = strings.Split(ts.w, " ")
if len(words) > 3 {
words = words[:4]
}
resp, total, err = ts.repo.SearchContents(ctx, words, tag.ID, "", -1, ts.page, ts.size, ts.order)
return
}

View File

@ -4,9 +4,11 @@ import (
"context"
"encoding/json"
"github.com/answerdev/answer/internal/base/constant"
"github.com/answerdev/answer/internal/service/activity_queue"
"github.com/answerdev/answer/internal/service/revision_common"
"github.com/answerdev/answer/internal/service/siteinfo_common"
"github.com/answerdev/answer/internal/service/tag_common"
tagcommonser "github.com/answerdev/answer/internal/service/tag_common"
"github.com/answerdev/answer/pkg/htmltext"
"github.com/answerdev/answer/internal/base/pager"
@ -21,17 +23,10 @@ import (
"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 TagRepo
tagCommonService *tag_common.TagCommonService
tagRepo tagcommonser.TagRepo
tagCommonService *tagcommonser.TagCommonService
revisionService *revision_common.RevisionService
followCommon activity_common.FollowRepo
siteInfoService *siteinfo_common.SiteInfoCommonService
@ -39,8 +34,8 @@ type TagService struct {
// NewTagService new tag service
func NewTagService(
tagRepo TagRepo,
tagCommonService *tag_common.TagCommonService,
tagRepo tagcommonser.TagRepo,
tagCommonService *tagcommonser.TagCommonService,
revisionService *revision_common.RevisionService,
followCommon activity_common.FollowRepo,
siteInfoService *siteinfo_common.SiteInfoCommonService) *TagService {
@ -54,62 +49,23 @@ func NewTagService(
}
// RemoveTag delete tag
func (ts *TagService) RemoveTag(ctx context.Context, tagID string) (err error) {
// TODO permission
err = ts.tagRepo.RemoveTag(ctx, tagID)
func (ts *TagService) RemoveTag(ctx context.Context, req *schema.RemoveTagReq) (err error) {
err = ts.tagRepo.RemoveTag(ctx, req.TagID)
if err != nil {
return err
}
activity_queue.AddActivity(&schema.ActivityMsg{
UserID: req.UserID,
ObjectID: req.TagID,
OriginalObjectID: req.TagID,
ActivityTypeKey: constant.ActTagDeleted,
})
return nil
}
// UpdateTag update tag
func (ts *TagService) UpdateTag(ctx context.Context, req *schema.UpdateTagReq) (err error) {
tag := &entity.Tag{}
_ = copier.Copy(tag, req)
tag.ID = req.TagID
err = ts.tagRepo.UpdateTag(ctx, tag)
if err != nil {
return err
}
tagInfo, exist, err := ts.tagCommonService.GetTagByID(ctx, req.TagID)
if err != nil {
return err
}
if !exist {
return errors.BadRequest(reason.TagNotFound)
}
if tagInfo.MainTagID == 0 && len(req.SlugName) > 0 {
log.Debugf("tag %s update slug_name", tagInfo.SlugName)
tagList, err := ts.tagRepo.GetTagList(ctx, &entity.Tag{MainTagID: converter.StringToInt64(tagInfo.ID)})
if err != nil {
return err
}
updateTagSlugNames := make([]string, 0)
for _, tag := range tagList {
updateTagSlugNames = append(updateTagSlugNames, tag.SlugName)
}
err = ts.tagRepo.UpdateTagSynonym(ctx, updateTagSlugNames, converter.StringToInt64(tagInfo.ID), tagInfo.MainTagSlugName)
if err != nil {
return err
}
}
revisionDTO := &schema.AddRevisionDTO{
UserID: req.UserID,
ObjectID: tag.ID,
Title: tag.SlugName,
Log: req.EditSummary,
}
tagInfoJson, _ := json.Marshal(tagInfo)
revisionDTO.Content = string(tagInfoJson)
err = ts.revisionService.AddRevision(ctx, revisionDTO, true)
if err != nil {
return err
}
return
return ts.tagCommonService.UpdateTag(ctx, req)
}
// GetTagInfo get tag one
@ -154,7 +110,7 @@ func (ts *TagService) GetTagInfo(ctx context.Context, req *schema.GetTagInfoReq)
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.MemberActions = permission.GetTagPermission(ctx, req.CanEdit, req.CanDelete)
resp.GetExcerpt()
return resp, nil
}
@ -198,7 +154,8 @@ 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) {
resp *schema.GetTagSynonymsResp, err error) {
resp = &schema.GetTagSynonymsResp{Synonyms: make([]*schema.TagSynonym, 0)}
tag, exist, err := ts.tagCommonService.GetTagByID(ctx, req.TagID)
if err != nil {
return
@ -230,15 +187,15 @@ func (ts *TagService) GetTagSynonyms(ctx context.Context, req *schema.GetTagSyno
mainTagSlugName = tag.SlugName
}
resp = make([]*schema.GetTagSynonymsResp, 0)
for _, t := range tagList {
resp = append(resp, &schema.GetTagSynonymsResp{
resp.Synonyms = append(resp.Synonyms, &schema.TagSynonym{
TagID: t.ID,
SlugName: t.SlugName,
DisplayName: t.DisplayName,
MainTagSlugName: mainTagSlugName,
})
}
resp.MemberActions = permission.GetTagSynonymPermission(ctx, req.CanEdit)
return
}
@ -281,6 +238,7 @@ func (ts *TagService) UpdateTagSynonym(ctx context.Context, req *schema.UpdateTa
item.OriginalText = tag.OriginalText
item.ParsedText = tag.ParsedText
item.Status = entity.TagStatusAvailable
item.UserID = req.UserID
needAddTagList = append(needAddTagList, item)
}
@ -299,10 +257,17 @@ func (ts *TagService) UpdateTagSynonym(ctx context.Context, req *schema.UpdateTa
}
tagInfoJson, _ := json.Marshal(tag)
revisionDTO.Content = string(tagInfoJson)
err = ts.revisionService.AddRevision(ctx, revisionDTO, true)
revisionID, err := ts.revisionService.AddRevision(ctx, revisionDTO, true)
if err != nil {
return err
}
activity_queue.AddActivity(&schema.ActivityMsg{
UserID: req.UserID,
ObjectID: tag.ID,
OriginalObjectID: tag.ID,
ActivityTypeKey: constant.ActTagCreated,
RevisionID: revisionID,
})
}
}
@ -339,6 +304,7 @@ func (ts *TagService) UpdateTagSynonym(ctx context.Context, req *schema.UpdateTa
func (ts *TagService) GetTagWithPage(ctx context.Context, req *schema.GetTagWithPageReq) (pageModel *pager.PageModel, err error) {
tag := &entity.Tag{}
_ = copier.Copy(tag, req)
tag.UserID = ""
page := req.Page
pageSize := req.PageSize

View File

@ -7,12 +7,15 @@ import (
"sort"
"strings"
"github.com/answerdev/answer/internal/base/constant"
"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/activity_queue"
"github.com/answerdev/answer/internal/service/revision_common"
"github.com/answerdev/answer/internal/service/siteinfo_common"
"github.com/answerdev/answer/pkg/converter"
"github.com/segmentfault/pacman/errors"
"github.com/segmentfault/pacman/log"
)
@ -23,7 +26,7 @@ type TagCommonRepo interface {
GetTagBySlugName(ctx context.Context, slugName string) (tagInfo *entity.Tag, exist bool, 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)
GetTagByID(ctx context.Context, tagID string) (tag *entity.Tag, exist bool, err error)
GetTagByID(ctx context.Context, tagID string, includeDeleted bool) (tag *entity.Tag, exist bool, 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)
@ -31,6 +34,13 @@ type TagCommonRepo interface {
UpdateTagQuestionCount(ctx context.Context, tagID string, questionCount int) (err error)
}
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)
}
type TagRelRepo interface {
AddTagRelList(ctx context.Context, tagList []*entity.TagRel) (err error)
RemoveTagRelListByIDs(ctx context.Context, ids []int64) (err error)
@ -46,17 +56,22 @@ type TagCommonService struct {
revisionService *revision_common.RevisionService
tagCommonRepo TagCommonRepo
tagRelRepo TagRelRepo
tagRepo TagRepo
siteInfoService *siteinfo_common.SiteInfoCommonService
}
// NewTagCommonService new tag service
func NewTagCommonService(tagCommonRepo TagCommonRepo, tagRelRepo TagRelRepo,
func NewTagCommonService(
tagCommonRepo TagCommonRepo,
tagRelRepo TagRelRepo,
tagRepo TagRepo,
revisionService *revision_common.RevisionService,
siteInfoService *siteinfo_common.SiteInfoCommonService,
) *TagCommonService {
return &TagCommonService{
tagCommonRepo: tagCommonRepo,
tagRelRepo: tagRelRepo,
tagRepo: tagRepo,
revisionService: revisionService,
siteInfoService: siteInfoService,
}
@ -68,7 +83,7 @@ func (ts *TagCommonService) SearchTagLike(ctx context.Context, req *schema.Searc
if err != nil {
return
}
ts.tagsFormatRecommendAndReserved(ctx, tags)
ts.TagsFormatRecommendAndReserved(ctx, tags)
for _, tag := range tags {
item := schema.SearchTagLikeResp{}
item.SlugName = tag.SlugName
@ -166,7 +181,7 @@ func (ts *TagCommonService) GetTagListByNames(ctx context.Context, tagNames []st
if err != nil {
return nil, err
}
ts.tagsFormatRecommendAndReserved(ctx, tagList)
ts.TagsFormatRecommendAndReserved(ctx, tagList)
return tagList, nil
}
@ -207,7 +222,7 @@ func (ts *TagCommonService) AddTagList(ctx context.Context, tagList []*entity.Ta
// 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)
tag, exist, err = ts.tagCommonRepo.GetTagByID(ctx, tagID, false)
if !exist {
return
}
@ -231,7 +246,7 @@ func (ts *TagCommonService) GetTagListByIDs(ctx context.Context, ids []string) (
if err != nil {
return nil, err
}
ts.tagsFormatRecommendAndReserved(ctx, tagList)
ts.TagsFormatRecommendAndReserved(ctx, tagList)
return
}
@ -242,7 +257,7 @@ func (ts *TagCommonService) GetTagPage(ctx context.Context, page, pageSize int,
if err != nil {
return nil, 0, err
}
ts.tagsFormatRecommendAndReserved(ctx, tagList)
ts.TagsFormatRecommendAndReserved(ctx, tagList)
return
}
@ -276,7 +291,7 @@ func (ts *TagCommonService) TagFormat(ctx context.Context, tags []*entity.Tag) (
return objTags, nil
}
func (ts *TagCommonService) tagsFormatRecommendAndReserved(ctx context.Context, tagList []*entity.Tag) {
func (ts *TagCommonService) TagsFormatRecommendAndReserved(ctx context.Context, tagList []*entity.Tag) {
if len(tagList) == 0 {
return
}
@ -389,6 +404,7 @@ func (ts *TagCommonService) CheckTag(ctx context.Context, tags []string, userID
item.OriginalText = ""
item.ParsedText = ""
item.Status = entity.TagStatusAvailable
item.UserID = userID
addTagList = append(addTagList, item)
addTagMsgList = append(addTagMsgList, tag)
}
@ -403,7 +419,31 @@ func (ts *TagCommonService) CheckTag(ctx context.Context, tags []string, userID
return nil
}
func (ts *TagCommonService) ObjectCheckChangeTag(ctx context.Context, oldobjectTagData, objectTagData []*entity.Tag) (bool, []string) {
// CheckTagsIsChange
func (ts *TagCommonService) CheckTagsIsChange(ctx context.Context, tagNameList, oldtagNameList []string) bool {
check := make(map[string]bool)
if len(tagNameList) != len(oldtagNameList) {
return true
}
for _, item := range tagNameList {
check[item] = false
}
for _, item := range oldtagNameList {
_, ok := check[item]
if !ok {
return true
}
check[item] = true
}
for _, value := range check {
if value == false {
return true
}
}
return false
}
func (ts *TagCommonService) CheckChangeReservedTag(ctx context.Context, oldobjectTagData, objectTagData []*entity.Tag) (bool, []string) {
reservedTagsMap := make(map[string]bool)
needTagsMap := make([]string, 0)
for _, tag := range objectTagData {
@ -463,6 +503,7 @@ func (ts *TagCommonService) ObjectChangeTag(ctx context.Context, objectTagData *
item.OriginalText = tag.OriginalText
item.ParsedText = tag.ParsedText
item.Status = entity.TagStatusAvailable
item.UserID = objectTagData.UserID
addTagList = append(addTagList, item)
}
@ -480,10 +521,17 @@ func (ts *TagCommonService) ObjectChangeTag(ctx context.Context, objectTagData *
}
tagInfoJson, _ := json.Marshal(tag)
revisionDTO.Content = string(tagInfoJson)
err = ts.revisionService.AddRevision(ctx, revisionDTO, true)
revisionID, err := ts.revisionService.AddRevision(ctx, revisionDTO, true)
if err != nil {
return err
}
activity_queue.AddActivity(&schema.ActivityMsg{
UserID: objectTagData.UserID,
ObjectID: tag.ID,
OriginalObjectID: tag.ID,
ActivityTypeKey: constant.ActTagCreated,
RevisionID: revisionID,
})
}
}
@ -573,3 +621,83 @@ func (ts *TagCommonService) CreateOrUpdateTagRelList(ctx context.Context, object
}
return nil
}
func (ts *TagCommonService) UpdateTag(ctx context.Context, req *schema.UpdateTagReq) (err error) {
var canUpdate bool
_, existUnreviewed, err := ts.revisionService.ExistUnreviewedByObjectID(ctx, req.TagID)
if err != nil {
return err
}
if existUnreviewed {
err = errors.BadRequest(reason.AnswerCannotUpdate)
return err
}
tagInfo, exist, err := ts.GetTagByID(ctx, req.TagID)
if err != nil {
return err
}
if !exist {
return errors.BadRequest(reason.TagNotFound)
}
//If the content is the same, ignore it
if tagInfo.OriginalText == req.OriginalText {
return nil
}
tagInfo.SlugName = req.SlugName
tagInfo.DisplayName = req.DisplayName
tagInfo.OriginalText = req.OriginalText
tagInfo.ParsedText = req.ParsedText
revisionDTO := &schema.AddRevisionDTO{
UserID: req.UserID,
ObjectID: tagInfo.ID,
Title: tagInfo.SlugName,
Log: req.EditSummary,
}
if req.NoNeedReview {
canUpdate = true
err = ts.tagRepo.UpdateTag(ctx, tagInfo)
if err != nil {
return err
}
if tagInfo.MainTagID == 0 && len(req.SlugName) > 0 {
log.Debugf("tag %s update slug_name", tagInfo.SlugName)
tagList, err := ts.tagRepo.GetTagList(ctx, &entity.Tag{MainTagID: converter.StringToInt64(tagInfo.ID)})
if err != nil {
return err
}
updateTagSlugNames := make([]string, 0)
for _, tag := range tagList {
updateTagSlugNames = append(updateTagSlugNames, tag.SlugName)
}
err = ts.tagRepo.UpdateTagSynonym(ctx, updateTagSlugNames, converter.StringToInt64(tagInfo.ID), tagInfo.MainTagSlugName)
if err != nil {
return err
}
}
revisionDTO.Status = entity.RevisionReviewPassStatus
} else {
revisionDTO.Status = entity.RevisionUnreviewedStatus
}
tagInfoJson, _ := json.Marshal(tagInfo)
revisionDTO.Content = string(tagInfoJson)
revisionID, err := ts.revisionService.AddRevision(ctx, revisionDTO, true)
if err != nil {
return err
}
if canUpdate {
activity_queue.AddActivity(&schema.ActivityMsg{
UserID: req.UserID,
ObjectID: tagInfo.ID,
OriginalObjectID: tagInfo.ID,
ActivityTypeKey: constant.ActTagEdited,
RevisionID: revisionID,
})
}
return
}

View File

@ -36,12 +36,13 @@ func NewUserCommon(userRepo UserRepo) *UserCommon {
}
}
func (us *UserCommon) GetUserBasicInfoByID(ctx context.Context, ID string) (*schema.UserBasicInfo, bool, error) {
func (us *UserCommon) GetUserBasicInfoByID(ctx context.Context, ID string) (
userBasicInfo *schema.UserBasicInfo, exist bool, err error) {
userInfo, exist, err := us.userRepo.GetByUserID(ctx, ID)
if err != nil {
return nil, exist, err
}
info := us.UserBasicInfoFormat(ctx, userInfo)
info := us.FormatUserBasicInfo(ctx, userInfo)
return info, exist, nil
}
@ -50,7 +51,7 @@ func (us *UserCommon) GetUserBasicInfoByUserName(ctx context.Context, username s
if err != nil {
return nil, exist, err
}
info := us.UserBasicInfoFormat(ctx, userInfo)
info := us.FormatUserBasicInfo(ctx, userInfo)
return info, exist, nil
}
@ -69,21 +70,21 @@ func (us *UserCommon) BatchUserBasicInfoByID(ctx context.Context, IDs []string)
return userMap, err
}
for _, item := range dbInfo {
info := us.UserBasicInfoFormat(ctx, item)
info := us.FormatUserBasicInfo(ctx, item)
userMap[item.ID] = info
}
return userMap, nil
}
// UserBasicInfoFormat
func (us *UserCommon) UserBasicInfoFormat(ctx context.Context, userInfo *entity.User) *schema.UserBasicInfo {
// FormatUserBasicInfo format user basic info
func (us *UserCommon) FormatUserBasicInfo(ctx context.Context, userInfo *entity.User) *schema.UserBasicInfo {
userBasicInfo := &schema.UserBasicInfo{}
Avatar := schema.FormatAvatarInfo(userInfo.Avatar)
userBasicInfo.ID = userInfo.ID
userBasicInfo.IsAdmin = userInfo.IsAdmin
userBasicInfo.Username = userInfo.Username
userBasicInfo.Rank = userInfo.Rank
userBasicInfo.DisplayName = userInfo.DisplayName
userBasicInfo.Avatar = Avatar
userBasicInfo.Avatar = schema.FormatAvatarInfo(userInfo.Avatar)
userBasicInfo.Website = userInfo.Website
userBasicInfo.Location = userInfo.Location
userBasicInfo.IPInfo = userInfo.IPInfo

View File

@ -10,6 +10,7 @@ import (
"strings"
"github.com/Chain-Zhang/pinyin"
"github.com/answerdev/answer/internal/base/handler"
"github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/internal/base/translator"
"github.com/answerdev/answer/internal/base/validator"
@ -486,7 +487,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 *validator.FormErrorField, err error) {
resp []*validator.FormErrorField, err error) {
userInfo, exist, err := us.userRepo.GetByUserID(ctx, req.UserID)
if err != nil {
return nil, err
@ -500,10 +501,10 @@ func (us *UserService) UserChangeEmailSendCode(ctx context.Context, req *schema.
return nil, err
}
if exist {
resp = &validator.FormErrorField{
resp = append([]*validator.FormErrorField{}, &validator.FormErrorField{
ErrorField: "e_mail",
ErrorMsg: reason.EmailDuplicate,
}
ErrorMsg: translator.GlobalTrans.Tr(handler.GetLangByCtx(ctx), reason.EmailDuplicate),
})
return resp, errors.BadRequest(reason.EmailDuplicate)
}

View File

@ -468,3 +468,8 @@ export interface UserRoleItem {
name: string;
description: string;
}
export interface MemberActionItem {
action: string;
name: string;
type: string;
}

View File

@ -1,6 +1,6 @@
import { memo, FC } from 'react';
import { Button } from 'react-bootstrap';
import { Link } from 'react-router-dom';
import { Link, useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { Modal } from '@/components';
@ -36,6 +36,7 @@ const Index: FC<IProps> = ({
}) => {
const { t } = useTranslation('translation', { keyPrefix: 'delete' });
const toast = useToast();
const navigate = useNavigate();
const reportModal = useReportModal();
const refershQuestion = () => {
@ -105,13 +106,14 @@ const Index: FC<IProps> = ({
});
}
};
const handleEdit = async (evt) => {
const handleEdit = (evt, targetUrl) => {
evt.preventDefault();
let checkObjectId = qid;
if (type === 'answer') {
checkObjectId = aid;
}
editCheck(checkObjectId).catch(() => {
evt.preventDefault();
editCheck(checkObjectId).then(() => {
navigate(targetUrl);
});
};
@ -165,7 +167,7 @@ const Index: FC<IProps> = ({
key={item.action}
to={editUrl}
className="link-secondary p-0 fs-14 me-3"
onClick={handleEdit}
onClick={(evt) => handleEdit(evt, editUrl)}
style={{ lineHeight: '23px' }}>
{item.name}
</Link>

View File

@ -117,7 +117,7 @@ const Users: FC = () => {
<thead>
<tr>
<th>{t('name')}</th>
{/* <th style={{ width: '12%' }}>{t('reputation')}</th> */}
<th style={{ width: '12%' }}>{t('reputation')}</th>
<th style={{ width: '20%' }}>{t('email')}</th>
<th className="text-nowrap" style={{ width: '15%' }}>
{t('created_at')}
@ -151,7 +151,6 @@ const Users: FC = () => {
showReputation={false}
/>
</td>
{/* <td>{user.rank}</td> */}
<td className="text-break">{user.e_mail}</td>
<td>
<FormatTime time={user.created_at} />
@ -183,7 +182,6 @@ const Users: FC = () => {
<Icon name="three-dots-vertical" />
</Dropdown.Toggle>
<Dropdown.Menu>
{/* <Dropdown.Item>{t('set_new_password')}</Dropdown.Item> */}
<Dropdown.Item
onClick={() => handleAction('status', user)}>
{t('change_status')}
@ -192,19 +190,8 @@ const Users: FC = () => {
onClick={() => handleAction('role', user)}>
{t('change_role')}
</Dropdown.Item>
{/* <Dropdown.Divider />
<Dropdown.Item>{t('show_logs')}</Dropdown.Item> */}
</Dropdown.Menu>
</Dropdown>
{/* {user.status !== 'deleted' && (
<Button
className="p-0 btn-no-border"
variant="link"
onClick={() => handleClick(user)}>
{t('change')}
</Button>
)} */}
</td>
) : null}
</tr>

View File

@ -235,8 +235,10 @@ const Ask = () => {
id: qid,
edit_summary: formData.edit_summary.value,
})
.then(() => {
navigate(pathFactory.questionLanding(qid, params.title));
.then((res) => {
navigate(pathFactory.questionLanding(qid, params.title), {
state: { isReview: res?.wait_for_review },
});
})
.catch((err) => {
if (err.isError) {

View File

@ -118,7 +118,8 @@ const Index: FC<Props> = ({
/>
</Col>
<Col lg={3} className="mb-3 mb-md-0">
{data.update_user_info?.username !== data.user_info?.username ? (
{data.update_user_info &&
data.update_user_info?.username !== data.user_info?.username ? (
<UserCard
data={data?.update_user_info}
time={Number(data.update_time)}

View File

@ -135,9 +135,10 @@ const Index: FC<Props> = ({ data, initPage, hasAnswer, isLogged }) => {
/>
</Col>
<Col lg={3} className="mb-3 mb-md-0">
{data.update_user_info?.username !== data.user_info?.username ? (
{data.update_user_info &&
data.update_user_info?.username !== data.user_info?.username ? (
<UserCard
data={data?.user_info}
data={data?.update_user_info}
time={data.edit_time}
preFix={t('edit')}
isLogged={isLogged}

View File

@ -1,10 +1,16 @@
import { useEffect, useState } from 'react';
import { Container, Row, Col } from 'react-bootstrap';
import { useParams, useSearchParams, useNavigate } from 'react-router-dom';
import {
useParams,
useSearchParams,
useNavigate,
useLocation,
} from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import Pattern from '@/common/pattern';
import { Pagination, PageTitle } from '@/components';
import { loggedUserInfoStore } from '@/stores';
import { loggedUserInfoStore, toastStore } from '@/stores';
import { scrollTop } from '@/utils';
import { usePageUsers } from '@/hooks';
import type {
@ -27,6 +33,7 @@ import './index.scss';
const Index = () => {
const navigate = useNavigate();
const { t } = useTranslation('translation');
const { qid = '', slugPermalink = '' } = useParams();
// Compatible with Permalink
let { aid = '' } = useParams();
@ -46,6 +53,17 @@ const Index = () => {
const userInfo = loggedUserInfoStore((state) => state.user);
const isAuthor = userInfo?.username === question?.user_info?.username;
const isLogged = Boolean(userInfo?.access_token);
const { state: locationState } = useLocation();
useEffect(() => {
if (locationState?.isReview) {
toastStore.getState().show({
msg: t('review', { keyPrefix: 'toast' }),
variant: 'warning',
});
}
}, [locationState]);
const requestAnswers = async () => {
const res = await getAnswers({
order: order === 'updated' ? order : 'default',

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