fix: guard user deletion with business data checks

This commit is contained in:
JetSprow
2026-04-29 17:30:17 +10:00
parent bae925b1fb
commit 340c93587e
2 changed files with 67 additions and 2 deletions

View File

@@ -21,6 +21,23 @@ const updateUserSchema = z.object({
role: z.enum(["ADMIN", "USER"]),
});
const userDeleteBlockerLabels: Record<string, string> = {
subscriptions: "订阅",
orders: "订单",
nodeClients: "节点客户端",
streamingSlots: "流媒体分配",
supportTickets: "工单",
inviteRewardLedgers: "邀请返利记录",
inviteeRewardLedgers: "被邀请返利记录",
};
function formatDeleteBlockers(counts: Record<string, number>) {
return Object.entries(counts)
.filter(([, count]) => count > 0)
.map(([key, count]) => `${userDeleteBlockerLabels[key] ?? key} ${count}`)
.join("、");
}
export async function createUser(formData: FormData) {
const session = await requireAdmin();
const data = createUserSchema.parse(Object.fromEntries(formData));
@@ -93,7 +110,55 @@ export async function updateUserStatus(id: string, status: "ACTIVE" | "DISABLED"
export async function deleteUser(id: string) {
const session = await requireAdmin();
const user = await prisma.user.delete({ where: { id } });
if (id === session.user.id) {
throw new Error("不能删除当前登录的管理员账号");
}
const user = await prisma.user.findUnique({
where: { id },
select: {
id: true,
email: true,
role: true,
_count: {
select: {
subscriptions: true,
orders: true,
nodeClients: true,
streamingSlots: true,
supportTickets: true,
inviteRewardLedgers: true,
inviteeRewardLedgers: true,
},
},
},
});
if (!user) {
throw new Error("用户不存在,可能已经被删除");
}
if (user.role === "ADMIN") {
const adminCount = await prisma.user.count({
where: {
role: "ADMIN",
status: "ACTIVE",
id: { not: user.id },
},
});
if (adminCount === 0) {
throw new Error("不能删除最后一个可用管理员账号");
}
}
const blockers = formatDeleteBlockers(user._count);
if (blockers) {
throw new Error(
`无法直接删除该用户:存在 ${blockers}。为避免订单、订阅和客服记录丢失,请改用“禁用”或“封禁”;如确需彻底清理,请先人工处理关联数据。`,
);
}
await prisma.user.delete({ where: { id: user.id } });
await recordAuditLog({
actor: actorFromSession(session),
action: "user.delete",

View File

@@ -63,7 +63,7 @@ export function UserActions({ user }: { user: User }) {
size="sm"
variant="destructive"
title="删除这个用户?"
description="用户账号及相关数据会被清理。请确认已完成必要备份。"
description="仅无业务数据的空账号会被物理删除;有订单、订阅或工单的用户请改用禁用/封禁。"
confirmLabel="删除用户"
successMessage="用户已删除"
errorMessage="删除失败"