实现用户注销和删除系统

This commit is contained in:
2025-05-18 22:25:05 +08:00
parent 32026c5673
commit 0d586f9aae
9 changed files with 87 additions and 30 deletions

View File

@@ -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 { }

View File

@@ -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')

View File

@@ -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;
}

View File

@@ -51,12 +51,23 @@ export class UserService {
}
}
async delete(userId: string): Promise<void> {
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 {

View File

@@ -75,7 +75,7 @@ export function CreateUserEditor({ children, onRefresh }: CreateUserEditorProps)
<Label htmlFor="password"></Label>
<Input id="password" name="password" />
</div>
<Button type="submit"></Button>
<Button type="submit"></Button>
</form>
<DrawerFooter className="pt-2">

View File

@@ -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
<AlertDialog>
<AlertDialogTrigger asChild>
<Button type="button" variant="destructive" className="flex-1"></Button>
<Button type="button" variant="destructive" className="flex-1"></Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>?</AlertDialogTitle>
<AlertDialogTitle>?</AlertDialogTitle>
<AlertDialogDescription>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel></AlertDialogCancel>
<AlertDialogAction onClick={() => onRemove(user.userId)}></AlertDialogAction>
<AlertDialogAction onClick={() => onRemove(user.userId)}></AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>

View File

@@ -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 (
<>
<div>
@@ -86,7 +105,12 @@ export default function Page() {
<TableCell>{user.email}</TableCell>
<TableCell>{user.phone}</TableCell>
<TableCell>
<Button className="cursor-pointer" variant='outline' size='sm' onClick={() => setEditorUserId(user.userId)}></Button>
{user.deletedAt
? <Button className="cursor-pointer" variant='destructive' size='sm'
onClick={() => setDeletedUserId(user.userId)}></Button>
: <Button className="cursor-pointer" variant='outline' size='sm'
onClick={() => setEditorUserId(user.userId)}></Button>
}
</TableCell>
</TableRow>
))
@@ -104,14 +128,29 @@ export default function Page() {
</TableRow>
)}
</TableBody>
</Table>
</Table >
<UserInfoEditor
onClose={() => setEditorUserId('')}
userId={editorUserId}
onUserUpdate={handleUserUpdate}
onUserDelete={handleUserDelete}
onUserUpdate={handleUserUpdateLocal}
onUserSoftDelete={userId => handleUserDeleteLocal(userId, true)}
/>
<AlertDialog open={!!deletedUserId} onOpenChange={o => !o && setDeletedUserId('')}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>?</AlertDialogTitle>
<AlertDialogDescription>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel></AlertDialogCancel>
<AlertDialogAction onClick={() => handleUserDelete(deletedUserId)}></AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</>
)
}

View File

@@ -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',
})
}

View File

@@ -7,4 +7,5 @@ export interface User {
avatar?: string;
createdAt: string;
updatedAt: string;
deletedAt: string | null;
}