fix: make risk unblock resilient

This commit is contained in:
JetSprow
2026-04-29 17:26:17 +10:00
parent 58fa4fefa4
commit bae925b1fb
2 changed files with 58 additions and 28 deletions

View File

@@ -9,6 +9,7 @@ import { prisma } from "@/lib/prisma";
import { requireAdmin } from "@/lib/require-auth";
import { actorFromSession, recordAuditLog } from "@/services/audit";
import { createNotification } from "@/services/notifications";
import { getErrorMessage } from "@/lib/errors";
import {
buildSubscriptionRiskReport,
getSubscriptionRiskAccessLogsForEvent,
@@ -160,6 +161,17 @@ async function restoreSubscriptionsForEvent(event: {
kind: "SINGLE" | "AGGREGATE";
}) {
const now = new Date();
const restoredSubscriptionIds: string[] = [];
const restorationErrors: string[] = [];
async function tryRestore(subscriptionId: string) {
try {
await activateSubscription(subscriptionId);
restoredSubscriptionIds.push(subscriptionId);
} catch (error) {
restorationErrors.push(`${subscriptionId}: ${getErrorMessage(error, "恢复订阅失败")}`);
}
}
if (event.kind === "SINGLE" && event.subscriptionId) {
const subscription = await prisma.userSubscription.findUnique({
@@ -167,10 +179,9 @@ async function restoreSubscriptionsForEvent(event: {
select: { id: true, status: true, endDate: true },
});
if (subscription?.status === "SUSPENDED" && subscription.endDate > now) {
await activateSubscription(subscription.id);
return [subscription.id];
await tryRestore(subscription.id);
}
return [];
return { restoredSubscriptionIds, restorationErrors };
}
if (event.kind === "AGGREGATE" && event.userId) {
@@ -185,15 +196,12 @@ async function restoreSubscriptionsForEvent(event: {
orderBy: { createdAt: "desc" },
});
const restoredIds: string[] = [];
for (const subscription of subscriptions) {
await activateSubscription(subscription.id);
restoredIds.push(subscription.id);
await tryRestore(subscription.id);
}
return restoredIds;
}
return [];
return { restoredSubscriptionIds, restorationErrors };
}
export async function updateSubscriptionRiskReview(
@@ -389,12 +397,13 @@ export async function finalizeSubscriptionRiskDecision(
report = generated.report;
}
const restoredSubscriptionIds = action === "RESTORE_ACCESS"
const { restoredSubscriptionIds, restorationErrors } = action === "RESTORE_ACCESS"
? await restoreSubscriptionsForEvent(event)
: [];
: { restoredSubscriptionIds: [] as string[], restorationErrors: [] as string[] };
const normalizedNote = normalizeNote(note);
const now = new Date();
const shouldKeepRestriction = action === "KEEP_RESTRICTED" && (event.userRestrictionActive || options.notifyUser);
let notificationError: string | null = null;
await prisma.subscriptionRiskEvent.update({
where: { id: event.id },
@@ -410,15 +419,11 @@ export async function finalizeSubscriptionRiskDecision(
finalActionByEmail: actor.email ?? null,
userRestrictionActive: shouldKeepRestriction,
userRestrictionResolvedAt: action === "RESTORE_ACCESS" ? now : event.userRestrictionResolvedAt,
...(options.notifyUser
? {
reportSentAt: event.reportSentAt ?? now,
}
: {}),
},
});
if (action === "RESTORE_ACCESS" && event.userId) {
try {
await createNotification({
userId: event.userId,
type: "SUBSCRIPTION",
@@ -428,10 +433,21 @@ export async function finalizeSubscriptionRiskDecision(
link: "/subscriptions",
dedupeKey: `risk:restriction-restored:${event.id}`,
});
} catch (error) {
notificationError = getErrorMessage(error, "解除限制通知发送失败");
}
}
if (action === "KEEP_RESTRICTED" && options.notifyUser && event.userId && report) {
try {
await notifyUserWithRiskReport({ eventId: event.id, userId: event.userId });
await prisma.subscriptionRiskEvent.update({
where: { id: event.id },
data: { reportSentAt: event.reportSentAt ?? now },
});
} catch (error) {
notificationError = getErrorMessage(error, "发送用户通知失败");
}
}
const targetLabel = await getRiskTargetLabel({
@@ -452,9 +468,11 @@ export async function finalizeSubscriptionRiskDecision(
notifyUser: options.notifyUser === true,
note: normalizedNote,
restoredSubscriptionIds,
restorationErrors,
notificationError,
},
});
revalidateRiskViews(event.subscriptionId, event.userId);
return { ok: true, restoredSubscriptionIds };
return { ok: true, restoredSubscriptionIds, restorationErrors, notificationError };
}

View File

@@ -183,10 +183,22 @@ export function SubscriptionRiskReviewActions({
startTransition(async () => {
try {
await finalizeSubscriptionRiskDecision(eventId, dialog.action, note, {
const result = await finalizeSubscriptionRiskDecision(eventId, dialog.action, note, {
notifyUser: dialog.action === "KEEP_RESTRICTED" && notifyUser,
});
if (result.restorationErrors.length > 0 || result.notificationError) {
const details = [
...result.restorationErrors.slice(0, 2),
result.notificationError,
].filter(Boolean).join("");
toast.warning(
dialog.action === "RESTORE_ACCESS"
? `限制已解除,但部分附带动作失败:${details}`
: `处置已保存,但通知发送异常:${details}`,
);
} else {
toast.success(dialog.action === "RESTORE_ACCESS" ? "已解除限制" : "已保持限制并记录处置");
}
setDialog(null);
router.refresh();
} catch (error) {