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