diff --git a/tone-page-server/src/admin/admin.module.ts b/tone-page-server/src/admin/admin.module.ts index a013564..524388b 100644 --- a/tone-page-server/src/admin/admin.module.ts +++ b/tone-page-server/src/admin/admin.module.ts @@ -1,7 +1,6 @@ import { Module } from '@nestjs/common'; import { AdminController } from './admin.controller'; import { AdminUserController } from './controller/admin-user.controller'; -import { AdminUserService } from './service/admin-user.service'; import { TypeOrmModule } from '@nestjs/typeorm'; import { User } from 'src/user/entities/user.entity'; import { UserModule } from 'src/user/user.module'; @@ -35,8 +34,5 @@ import { BlogModule } from 'src/blog/blog.module'; AdminWebResourceController, AdminWebBlogController, ], - providers: [ - AdminUserService, - ] }) export class AdminModule { } diff --git a/tone-page-server/src/admin/controller/admin-user.controller.ts b/tone-page-server/src/admin/controller/admin-user.controller.ts index c791221..acfe19f 100644 --- a/tone-page-server/src/admin/controller/admin-user.controller.ts +++ b/tone-page-server/src/admin/controller/admin-user.controller.ts @@ -4,6 +4,7 @@ import { CreateDto } from "../dto/admin-user/create.dto"; import { UserService } from "src/user/user.service"; import { UpdateDto } from "../dto/admin-user/update.dto"; import { UpdatePasswordDto } from "../dto/admin-user/update-password.dto"; +import { RemoveUserDto } from "../dto/admin-user/remove.dto"; @Controller('admin/user') export class AdminUserController { @@ -53,8 +54,9 @@ export class AdminUserController { @Delete(':userId') async delete( @Param('userId', new ParseUUIDPipe({ version: '4' })) userId: string, + @Query() dto: RemoveUserDto, ) { - return this.userService.delete(userId); + return this.userService.delete(userId, dto.soft); } @Post(':userId/password') diff --git a/tone-page-server/src/admin/dto/admin-user/remove.dto.ts b/tone-page-server/src/admin/dto/admin-user/remove.dto.ts new file mode 100644 index 0000000..9ed5da0 --- /dev/null +++ b/tone-page-server/src/admin/dto/admin-user/remove.dto.ts @@ -0,0 +1,8 @@ +import { Transform } from "class-transformer"; +import { IsBoolean } from "class-validator"; + +export class RemoveUserDto { + @Transform(({ value }) => value === 'true') + @IsBoolean({ message: '需指定删除类型' }) + soft: boolean; +} \ No newline at end of file diff --git a/tone-page-server/src/user/user.service.ts b/tone-page-server/src/user/user.service.ts index 2952f4c..366003f 100644 --- a/tone-page-server/src/user/user.service.ts +++ b/tone-page-server/src/user/user.service.ts @@ -51,12 +51,23 @@ export class UserService { } } - async delete(userId: string): Promise { - const existingUser = await this.userRepository.findOne({ where: { userId } }); + async delete(userId: string, soft: boolean) { + const existingUser = await this.userRepository.findOne({ where: { userId }, withDeleted: true }); if (!existingUser) { - throw new BadRequestException('User not found'); + throw new BadRequestException('用户不存在'); } - await this.userRepository.softDelete(existingUser.userId); + + if (existingUser.deletedAt && soft) { + throw new BadRequestException('账户已注销,不得重复操作') + } + + if (!existingUser.deletedAt && !soft) { + throw new BadRequestException('账号未注销,请先注销再执行删除操作') + } + + return soft + ? await this.userRepository.softDelete(existingUser.userId) + : await this.userRepository.delete(existingUser.userId) } hashPassword(password: string, salt: string): string { diff --git a/tone-page-web/app/console/(with-menu)/user/list/components/create-user-editor.tsx b/tone-page-web/app/console/(with-menu)/user/list/components/create-user-editor.tsx index 9c0841d..a09dbe8 100644 --- a/tone-page-web/app/console/(with-menu)/user/list/components/create-user-editor.tsx +++ b/tone-page-web/app/console/(with-menu)/user/list/components/create-user-editor.tsx @@ -75,7 +75,7 @@ export function CreateUserEditor({ children, onRefresh }: CreateUserEditorProps) - + diff --git a/tone-page-web/app/console/(with-menu)/user/list/components/user-info-editor.tsx b/tone-page-web/app/console/(with-menu)/user/list/components/user-info-editor.tsx index 17717f5..46494f0 100644 --- a/tone-page-web/app/console/(with-menu)/user/list/components/user-info-editor.tsx +++ b/tone-page-web/app/console/(with-menu)/user/list/components/user-info-editor.tsx @@ -27,17 +27,17 @@ import { } from "@/components/ui/alert" import { AlertCircle } from "lucide-react"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from "@/components/ui/alert-dialog"; -import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; +import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; export function UserInfoEditor({ onClose, onUserUpdate, - onUserDelete, + onUserSoftDelete, userId, }: { onClose: () => void, onUserUpdate: (user: User) => void, - onUserDelete: (userId: string) => void, + onUserSoftDelete: (userId: string) => void, userId: string }) { const { user, isLoading, error } = useUser(userId); @@ -65,12 +65,12 @@ export function UserInfoEditor({ const handleRemove = async (userId: string) => { try { setRemoveLoading(true); - await AdminApi.user.remove(userId); - toast.success("删除成功"); - onUserDelete(userId); + await AdminApi.user.remove(userId, true); + toast.success("注销成功"); + onUserSoftDelete(userId); onClose(); } catch (error) { - toast.error((error as Error).message || "删除失败"); + toast.error((error as Error).message || "注销失败"); } finally { setRemoveLoading(false); } @@ -208,18 +208,18 @@ function ProfileForm({ className, user, onSetPassword, onRemove, passwordDialogO - + - 是否要删除该用户? + 是否要注销该账号? - 该操作无法撤销,这会永久删除该用户的所有数据 + 该操作无法撤销,稍后可通过删除来彻底清理该用户信息 取消 - onRemove(user.userId)}>删除 + onRemove(user.userId)}>注销 diff --git a/tone-page-web/app/console/(with-menu)/user/list/page.tsx b/tone-page-web/app/console/(with-menu)/user/list/page.tsx index 0ff7658..a369c7a 100644 --- a/tone-page-web/app/console/(with-menu)/user/list/page.tsx +++ b/tone-page-web/app/console/(with-menu)/user/list/page.tsx @@ -8,13 +8,17 @@ import { useState } from "react"; import { UserInfoEditor } from "./components/user-info-editor"; import { User } from "@/lib/types/user"; import { CreateUserEditor } from "./components/create-user-editor"; +import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from "@/components/ui/alert-dialog"; +import { AdminApi } from "@/lib/api"; +import { toast } from "sonner"; +import { ApiError } from "next/dist/server/api-utils"; export default function Page() { const { users, isLoading, error, total, page, pageSize, mutate, refresh } = useUserList(); const [editorUserId, setEditorUserId] = useState(""); - const handleUserUpdate = async (newUser: User) => { + const handleUserUpdateLocal = async (newUser: User) => { await mutate( (data) => { if (!data) return data; @@ -31,11 +35,14 @@ export default function Page() { ) } - const handleUserDelete = async (userId: string) => { + const handleUserDeleteLocal = async (userId: string, soft: boolean) => { await mutate( (data) => { if (!data) return data; - return { + return soft ? { + ...data, + items: data.items.map(u => u.userId === userId ? { ...u, deletedAt: new Date().toLocaleString() } : u) + } : { ...data, items: data.items.filter((user) => user.userId !== userId), }; @@ -46,6 +53,18 @@ export default function Page() { ) } + const [deletedUserId, setDeletedUserId] = useState(''); + const handleUserDelete = async (userId: string) => { + try { + await AdminApi.user.remove(userId, false); + toast.success('删除成功'); + handleUserDeleteLocal(userId, false); + setDeletedUserId(''); + } catch (error) { + toast.error((error as ApiError).message || '删除失败'); + } + } + return ( <>
@@ -86,7 +105,12 @@ export default function Page() { {user.email} {user.phone} - + {user.deletedAt + ? + : + } )) @@ -104,14 +128,29 @@ export default function Page() { )} - + setEditorUserId('')} userId={editorUserId} - onUserUpdate={handleUserUpdate} - onUserDelete={handleUserDelete} + onUserUpdate={handleUserUpdateLocal} + onUserSoftDelete={userId => handleUserDeleteLocal(userId, true)} /> + + !o && setDeletedUserId('')}> + + + 是否要彻底删除账号? + + 该操作无法撤销,会彻底删除该账号相关连的所有数据 + + + + 取消 + handleUserDelete(deletedUserId)}>删除 + + + ) } \ No newline at end of file diff --git a/tone-page-web/lib/api/admin/user/remove.ts b/tone-page-web/lib/api/admin/user/remove.ts index 6f1bf54..5ec1b66 100644 --- a/tone-page-web/lib/api/admin/user/remove.ts +++ b/tone-page-web/lib/api/admin/user/remove.ts @@ -1,7 +1,7 @@ import fetcher from "../../fetcher"; -export async function remove(userId: string) { - return fetcher(`/api/admin/user/${userId}`, { +export async function remove(userId: string, soft: boolean) { + return fetcher(`/api/admin/user/${userId}?soft=${soft}`, { method: 'DELETE', }) } \ No newline at end of file diff --git a/tone-page-web/lib/types/user.ts b/tone-page-web/lib/types/user.ts index fcfca8d..16c56e5 100644 --- a/tone-page-web/lib/types/user.ts +++ b/tone-page-web/lib/types/user.ts @@ -7,4 +7,5 @@ export interface User { avatar?: string; createdAt: string; updatedAt: string; + deletedAt: string | null; } \ No newline at end of file