This is an automated email from the ASF dual-hosted git repository. shuai pushed a commit to branch dev in repository https://gitbox.apache.org/repos/asf/incubator-answer.git
commit 4a74eed4a7de8589fe50d1f8568d4c141da69850 Author: Luffy <[email protected]> AuthorDate: Wed Dec 18 18:34:10 2024 +0800 feat: Add permanently delete --- docs/docs.go | 55 ++++++++++++++++++++++ docs/swagger.json | 55 ++++++++++++++++++++++ docs/swagger.yaml | 35 ++++++++++++++ i18n/en_US.yaml | 9 +++- i18n/zh_CN.yaml | 9 +++- internal/base/constant/user.go | 6 +++ .../controller_admin/user_backyard_controller.go | 20 ++++++++ internal/repo/answer/answer_repo.go | 8 ++++ internal/repo/question/question_repo.go | 8 ++++ internal/repo/user/user_backyard_repo.go | 9 ++++ internal/router/answer_api_router.go | 2 + internal/schema/backyard_user_schema.go | 5 ++ internal/service/answer_common/answer.go | 1 + internal/service/question_common/question.go | 1 + internal/service/user_admin/user_backyard.go | 13 +++++ ui/src/pages/Admin/Answers/index.tsx | 46 ++++++++++++++---- ui/src/pages/Admin/Questions/index.tsx | 46 ++++++++++++++---- ui/src/pages/Admin/Users/index.tsx | 28 +++++++++++ ui/src/services/common.ts | 4 ++ 19 files changed, 340 insertions(+), 20 deletions(-) diff --git a/docs/docs.go b/docs/docs.go index da050e10..10930067 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -295,6 +295,45 @@ const docTemplate = `{ } } }, + "/answer/admin/api/delete/permanently": { + "delete": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "delete permanently", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "delete permanently", + "parameters": [ + { + "description": "DeletePermanentlyReq", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.DeletePermanentlyReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, "/answer/admin/api/language/options": { "get": { "description": "Get language options", @@ -8158,6 +8197,22 @@ const docTemplate = `{ } } }, + "schema.DeletePermanentlyReq": { + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "users", + "questions", + "answers" + ] + } + } + }, "schema.EditUserProfileReq": { "type": "object", "required": [ diff --git a/docs/swagger.json b/docs/swagger.json index bcce7817..53b95cb8 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -268,6 +268,45 @@ } } }, + "/answer/admin/api/delete/permanently": { + "delete": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "delete permanently", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "delete permanently", + "parameters": [ + { + "description": "DeletePermanentlyReq", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.DeletePermanentlyReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, "/answer/admin/api/language/options": { "get": { "description": "Get language options", @@ -8131,6 +8170,22 @@ } } }, + "schema.DeletePermanentlyReq": { + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "users", + "questions", + "answers" + ] + } + } + }, "schema.EditUserProfileReq": { "type": "object", "required": [ diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 5e22186a..55b18c5e 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -495,6 +495,17 @@ definitions: name: type: string type: object + schema.DeletePermanentlyReq: + properties: + type: + enum: + - users + - questions + - answers + type: string + required: + - type + type: object schema.EditUserProfileReq: properties: display_name: @@ -3106,6 +3117,30 @@ paths: summary: DashboardInfo tags: - admin + /answer/admin/api/delete/permanently: + delete: + consumes: + - application/json + description: delete permanently + parameters: + - description: DeletePermanentlyReq + in: body + name: data + required: true + schema: + $ref: '#/definitions/schema.DeletePermanentlyReq' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handler.RespBody' + security: + - ApiKeyAuth: [] + summary: delete permanently + tags: + - admin /answer/admin/api/language/options: get: description: Get language options diff --git a/i18n/en_US.yaml b/i18n/en_US.yaml index 801acc69..9d904622 100644 --- a/i18n/en_US.yaml +++ b/i18n/en_US.yaml @@ -1504,6 +1504,7 @@ ui: normal: Normal closed: Closed deleted: Deleted + deleted_permanently: Deleted permanently pending: Pending more: More search: @@ -1539,6 +1540,9 @@ ui: cannot_vote_for_self: You can't vote for your own post. modal_confirm: title: Error... + delete_permanently: + title: Delete permanently + content: Are you sure you want to delete permanently? account_result: success: Your new account is confirmed; you will be redirected to the home page. link: Continue to homepage @@ -2297,5 +2301,6 @@ ui: user_deleted: This user has been deleted. badge_activated: This badge has been activated. badge_inactivated: This badge has been inactivated. - - + users_deleted: These users have been deleted. + posts_deleted: These questions have been deleted. + answers_deleted: These answers have been deleted. diff --git a/i18n/zh_CN.yaml b/i18n/zh_CN.yaml index 0567a10c..d06733fb 100644 --- a/i18n/zh_CN.yaml +++ b/i18n/zh_CN.yaml @@ -1232,6 +1232,9 @@ ui: modal_content: 该电子邮件地址已经注册。你确定要连接到已有账户吗? modal_cancel: 更改邮箱 modal_confirm: 连接到已有账户 + delete_permanently: + title: 永久删除 + content: 你确定要永久删除吗? password_reset: page_title: 密码重置 btn_name: 重置我的密码 @@ -1471,6 +1474,7 @@ ui: normal: 正常 closed: 已关闭 deleted: 已删除 + deleted_permanently: 永久删除 pending: 等待处理 more: 更多 search: @@ -2259,5 +2263,6 @@ ui: user_deleted: 此用户已被删除 badge_activated: 此徽章已被激活。 badge_inactivated: 此徽章已被禁用。 - - + users_deleted: 这些用户已被删除。 + posts_deleted: 这些帖子已被删除。 + answers_deleted: 这些回答已被删除。 diff --git a/internal/base/constant/user.go b/internal/base/constant/user.go index d453e443..80774e0d 100644 --- a/internal/base/constant/user.go +++ b/internal/base/constant/user.go @@ -30,6 +30,12 @@ const ( EmailStatusToBeVerified = 2 ) +const ( + DeletePermanentlyUsers = "users" + DeletePermanentlyQuestions = "questions" + DeletePermanentlyAnswers = "answers" +) + func ConvertUserStatus(status, mailStatus int) string { switch status { case 1: diff --git a/internal/controller_admin/user_backyard_controller.go b/internal/controller_admin/user_backyard_controller.go index a2ab3f2e..1d9fb612 100644 --- a/internal/controller_admin/user_backyard_controller.go +++ b/internal/controller_admin/user_backyard_controller.go @@ -242,3 +242,23 @@ func (uc *UserAdminController) SendUserActivation(ctx *gin.Context) { err := uc.userService.SendUserActivation(ctx, req) handler.HandleResponse(ctx, err, nil) } + +// DeletePermanently delete permanently +// @Summary delete permanently +// @Description delete permanently +// @Security ApiKeyAuth +// @Tags admin +// @Accept json +// @Produce json +// @Param data body schema.DeletePermanentlyReq true "DeletePermanentlyReq" +// @Success 200 {object} handler.RespBody +// @Router /answer/admin/api/delete/permanently [delete] +func (uc *UserAdminController) DeletePermanently(ctx *gin.Context) { + req := &schema.DeletePermanentlyReq{} + if handler.BindAndCheck(ctx, req) { + return + } + + err := uc.userService.DeletePermanently(ctx, req) + handler.HandleResponse(ctx, err, nil) +} diff --git a/internal/repo/answer/answer_repo.go b/internal/repo/answer/answer_repo.go index f59d6006..f4bba8cc 100644 --- a/internal/repo/answer/answer_repo.go +++ b/internal/repo/answer/answer_repo.go @@ -527,3 +527,11 @@ func (ar *answerRepo) updateSearch(ctx context.Context, answerID string) (err er err = s.UpdateContent(ctx, content) return } + +func (ar *answerRepo) DeletePermanentlyAnswers(ctx context.Context) error { + _, err := ar.data.DB.Context(ctx).Where("status = ?", entity.AnswerStatusDeleted).Delete(&entity.Answer{}) + if err != nil { + return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return nil +} diff --git a/internal/repo/question/question_repo.go b/internal/repo/question/question_repo.go index 9b1d212e..7000e75f 100644 --- a/internal/repo/question/question_repo.go +++ b/internal/repo/question/question_repo.go @@ -167,6 +167,14 @@ func (qr *questionRepo) UpdateQuestionStatusWithOutUpdateTime(ctx context.Contex return nil } +func (qr *questionRepo) DeletePermanentlyQuestions(ctx context.Context) (err error) { + _, err = qr.data.DB.Context(ctx).Where("status = ?", entity.QuestionStatusDeleted).Delete(&entity.Question{}) + if err != nil { + return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return nil +} + func (qr *questionRepo) RecoverQuestion(ctx context.Context, questionID string) (err error) { questionID = uid.DeShortID(questionID) _, err = qr.data.DB.Context(ctx).ID(questionID).Cols("status").Update(&entity.Question{Status: entity.QuestionStatusAvailable}) diff --git a/internal/repo/user/user_backyard_repo.go b/internal/repo/user/user_backyard_repo.go index 44a05cfb..62f7f789 100644 --- a/internal/repo/user/user_backyard_repo.go +++ b/internal/repo/user/user_backyard_repo.go @@ -175,3 +175,12 @@ func (ur *userAdminRepo) GetUserPage(ctx context.Context, page, pageSize int, us tryToDecorateUserListFromUserCenter(ctx, ur.data, users) return } + +// DeletePermanentlyUsers delete permanently users +func (ur *userAdminRepo) DeletePermanentlyUsers(ctx context.Context) (err error) { + _, err = ur.data.DB.Context(ctx).Where("deleted_at IS NOT NULL").Delete(&entity.User{}) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return +} diff --git a/internal/router/answer_api_router.go b/internal/router/answer_api_router.go index 2927afef..b090f9c7 100644 --- a/internal/router/answer_api_router.go +++ b/internal/router/answer_api_router.go @@ -328,6 +328,8 @@ func (a *AnswerAPIRouter) RegisterAnswerAdminAPIRouter(r *gin.RouterGroup) { r.PUT("/user/password", a.adminUserController.UpdateUserPassword) r.PUT("/user/profile", a.adminUserController.EditUserProfile) + r.DELETE("/delete/permanently", a.adminUserController.DeletePermanently) + // reason r.GET("/reasons", a.reasonController.Reasons) diff --git a/internal/schema/backyard_user_schema.go b/internal/schema/backyard_user_schema.go index 7c690aee..966665a4 100644 --- a/internal/schema/backyard_user_schema.go +++ b/internal/schema/backyard_user_schema.go @@ -133,6 +133,11 @@ type AddUsersReq struct { Users []*AddUserReq `json:"-"` } +// DeletePermanentlyReq delete permanently request +type DeletePermanentlyReq struct { + Type string `validate:"required,oneof=users questions answers" json:"type"` +} + type AddUsersErrorData struct { // optional. error field name. Field string `json:"field"` diff --git a/internal/service/answer_common/answer.go b/internal/service/answer_common/answer.go index 45b11886..be40fe48 100644 --- a/internal/service/answer_common/answer.go +++ b/internal/service/answer_common/answer.go @@ -51,6 +51,7 @@ type AnswerRepo interface { GetAnswerCount(ctx context.Context) (count int64, err error) RemoveAllUserAnswer(ctx context.Context, userID string) (err error) SumVotesByQuestionID(ctx context.Context, questionID string) (float64, error) + DeletePermanentlyAnswers(ctx context.Context) (err error) } // AnswerCommon user service diff --git a/internal/service/question_common/question.go b/internal/service/question_common/question.go index fc01159e..c5a4fd15 100644 --- a/internal/service/question_common/question.go +++ b/internal/service/question_common/question.go @@ -63,6 +63,7 @@ type QuestionRepo interface { GetRecommendQuestionPageByTags(ctx context.Context, userID string, tagIDs, followedQuestionIDs []string, page, pageSize int) (questionList []*entity.Question, total int64, err error) UpdateQuestionStatus(ctx context.Context, questionID string, status int) (err error) UpdateQuestionStatusWithOutUpdateTime(ctx context.Context, question *entity.Question) (err error) + DeletePermanentlyQuestions(ctx context.Context) (err error) RecoverQuestion(ctx context.Context, questionID string) (err error) UpdateQuestionOperation(ctx context.Context, question *entity.Question) (err error) GetQuestionsByTitle(ctx context.Context, title string, pageSize int) (questionList []*entity.Question, err error) diff --git a/internal/service/user_admin/user_backyard.go b/internal/service/user_admin/user_backyard.go index ebe1ea74..e7f63d5f 100644 --- a/internal/service/user_admin/user_backyard.go +++ b/internal/service/user_admin/user_backyard.go @@ -63,6 +63,7 @@ type UserAdminRepo interface { AddUser(ctx context.Context, user *entity.User) (err error) AddUsers(ctx context.Context, users []*entity.User) (err error) UpdateUserPassword(ctx context.Context, userID string, password string) (err error) + DeletePermanentlyUsers(ctx context.Context) (err error) } // UserAdminService user service @@ -578,3 +579,15 @@ func (us *UserAdminService) SendUserActivation(ctx context.Context, req *schema. go us.emailService.SendAndSaveCode(ctx, userInfo.ID, userInfo.EMail, title, body, code, data.ToJSONString()) return nil } + +func (us *UserAdminService) DeletePermanently(ctx context.Context, req *schema.DeletePermanentlyReq) (err error) { + if req.Type == constant.DeletePermanentlyUsers { + return us.userRepo.DeletePermanentlyUsers(ctx) + } else if req.Type == constant.DeletePermanentlyQuestions { + return us.questionCommonRepo.DeletePermanentlyQuestions(ctx) + } else if req.Type == constant.DeletePermanentlyAnswers { + return us.answerCommonRepo.DeletePermanentlyAnswers(ctx) + } + + return errors.BadRequest(reason.RequestFormatError) +} diff --git a/ui/src/pages/Admin/Answers/index.tsx b/ui/src/pages/Admin/Answers/index.tsx index 7f7e6fae..7ab2e80a 100644 --- a/ui/src/pages/Admin/Answers/index.tsx +++ b/ui/src/pages/Admin/Answers/index.tsx @@ -18,7 +18,7 @@ */ import { FC } from 'react'; -import { Form, Table, Stack } from 'react-bootstrap'; +import { Form, Table, Stack, Button } from 'react-bootstrap'; import { useSearchParams, Link } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; @@ -31,12 +31,14 @@ import { BaseUserCard, Empty, QueryGroup, + Modal, } from '@/components'; import { ADMIN_LIST_STATUS } from '@/common/constants'; import * as Type from '@/common/interface'; -import { useAnswerSearch } from '@/services'; +import { deletePermanently, useAnswerSearch } from '@/services'; import { escapeRemove } from '@/utils'; import { pathFactory } from '@/router/pathFactory'; +import { toastStore } from '@/stores'; import AnswerAction from './components/Action'; @@ -68,6 +70,24 @@ const Answers: FC = () => { }); const count = listData?.count || 0; + const handleDeletePermanently = () => { + Modal.confirm({ + title: t('title', { keyPrefix: 'delete_permanently' }), + content: t('content', { keyPrefix: 'delete_permanently' }), + cancelBtnVariant: 'link', + confirmText: t('ok', { keyPrefix: 'btns' }), + onConfirm: () => { + deletePermanently('answers').then(() => { + toastStore.getState().show({ + msg: t('answers_deleted', { keyPrefix: 'messages' }), + variant: 'success', + }); + refreshList(); + }); + }, + }); + }; + const handleFilter = (e) => { urlSearchParams.set('query', e.target.value); urlSearchParams.delete('page'); @@ -77,12 +97,22 @@ const Answers: FC = () => { <> <h3 className="mb-4">{t('page_title')}</h3> <div className="d-flex flex-wrap justify-content-between align-items-center mb-3"> - <QueryGroup - data={answerFilterItems} - currentSort={curFilter} - sortKey="status" - i18nKeyPrefix="btns" - /> + <Stack direction="horizontal" gap={3}> + <QueryGroup + data={answerFilterItems} + currentSort={curFilter} + sortKey="status" + i18nKeyPrefix="btns" + /> + {curFilter === 'deleted' ? ( + <Button + variant="outline-danger" + size="sm" + onClick={() => handleDeletePermanently()}> + {t('deleted_permanently', { keyPrefix: 'btns' })} + </Button> + ) : null} + </Stack> <Form.Control value={curQuery} diff --git a/ui/src/pages/Admin/Questions/index.tsx b/ui/src/pages/Admin/Questions/index.tsx index 0e6442cf..116984b2 100644 --- a/ui/src/pages/Admin/Questions/index.tsx +++ b/ui/src/pages/Admin/Questions/index.tsx @@ -18,7 +18,7 @@ */ import { FC } from 'react'; -import { Form, Table, Stack } from 'react-bootstrap'; +import { Form, Table, Stack, Button } from 'react-bootstrap'; import { Link, useSearchParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; @@ -31,11 +31,13 @@ import { BaseUserCard, Empty, QueryGroup, + Modal, } from '@/components'; import { ADMIN_LIST_STATUS } from '@/common/constants'; import * as Type from '@/common/interface'; -import { useQuestionSearch } from '@/services'; +import { deletePermanently, useQuestionSearch } from '@/services'; import { pathFactory } from '@/router/pathFactory'; +import { toastStore } from '@/stores'; import Action from './components/Action'; @@ -66,6 +68,24 @@ const Questions: FC = () => { }); const count = listData?.count || 0; + const handleDeletePermanently = () => { + Modal.confirm({ + title: t('title', { keyPrefix: 'delete_permanently' }), + content: t('content', { keyPrefix: 'delete_permanently' }), + cancelBtnVariant: 'link', + confirmText: t('ok', { keyPrefix: 'btns' }), + onConfirm: () => { + deletePermanently('questions').then(() => { + toastStore.getState().show({ + msg: t('posts_deleted', { keyPrefix: 'messages' }), + variant: 'success', + }); + refreshList(); + }); + }, + }); + }; + const handleFilter = (e) => { urlSearchParams.set('query', e.target.value); urlSearchParams.delete('page'); @@ -75,12 +95,22 @@ const Questions: FC = () => { <> <h3 className="mb-4">{t('page_title')}</h3> <div className="d-flex flex-wrap justify-content-between align-items-center mb-3"> - <QueryGroup - data={questionFilterItems} - currentSort={curFilter} - sortKey="status" - i18nKeyPrefix="btns" - /> + <Stack direction="horizontal" gap={3}> + <QueryGroup + data={questionFilterItems} + currentSort={curFilter} + sortKey="status" + i18nKeyPrefix="btns" + /> + {curFilter === 'deleted' ? ( + <Button + variant="outline-danger" + size="sm" + onClick={() => handleDeletePermanently()}> + {t('deleted_permanently', { keyPrefix: 'btns' })} + </Button> + ) : null} + </Stack> <Form.Control value={curQuery} diff --git a/ui/src/pages/Admin/Users/index.tsx b/ui/src/pages/Admin/Users/index.tsx index 40152200..f97a4b5e 100644 --- a/ui/src/pages/Admin/Users/index.tsx +++ b/ui/src/pages/Admin/Users/index.tsx @@ -30,6 +30,7 @@ import { BaseUserCard, Empty, QueryGroup, + Modal, } from '@/components'; import * as Type from '@/common/interface'; import { useUserModal } from '@/hooks'; @@ -40,6 +41,7 @@ import { getAdminUcAgent, AdminUcAgent, changeUserStatus, + deletePermanently, } from '@/services'; import { formatCount } from '@/utils'; @@ -151,6 +153,24 @@ const Users: FC = () => { }); }; + const handleDeletePermanently = () => { + Modal.confirm({ + title: t('title', { keyPrefix: 'delete_permanently' }), + content: t('content', { keyPrefix: 'delete_permanently' }), + cancelBtnVariant: 'link', + confirmText: t('ok', { keyPrefix: 'btns' }), + onConfirm: () => { + deletePermanently('users').then(() => { + toastStore.getState().show({ + msg: t('users_deleted', { keyPrefix: 'messages' }), + variant: 'success', + }); + refreshUsers(); + }); + }, + }); + }; + const showAddUser = !ucAgent?.enabled || (ucAgent?.enabled && adminUcAgent?.allow_create_user); const showActionPassword = @@ -177,6 +197,14 @@ const Users: FC = () => { sortKey="filter" i18nKeyPrefix="admin.users" /> + {curFilter === 'deleted' ? ( + <Button + variant="outline-danger" + size="sm" + onClick={() => handleDeletePermanently()}> + {t('deleted_permanently', { keyPrefix: 'btns' })} + </Button> + ) : null} {showAddUser ? ( <Button variant="outline-primary" diff --git a/ui/src/services/common.ts b/ui/src/services/common.ts index 4ab940a2..cac34b4f 100644 --- a/ui/src/services/common.ts +++ b/ui/src/services/common.ts @@ -342,3 +342,7 @@ export const questionOperation = (params: Type.QuestionOperationReq) => { export const getPluginsStatus = () => { return request.get<Type.ActivatedPlugin[]>('/answer/api/v1/plugin/status'); }; + +export const deletePermanently = (type: string) => { + return request.delete('/answer/admin/api/delete/permanently', { type }); +};
