mirror of
https://github.com/JetSprow/J-Board-Lite.git
synced 2026-05-01 01:14:10 +05:30
fix: make risk unblock resilient
This commit is contained in:
@@ -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,28 +419,35 @@ 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) {
|
||||||
await createNotification({
|
try {
|
||||||
userId: event.userId,
|
await createNotification({
|
||||||
type: "SUBSCRIPTION",
|
userId: event.userId,
|
||||||
level: "SUCCESS",
|
type: "SUBSCRIPTION",
|
||||||
title: "订阅风控限制已解除",
|
level: "SUCCESS",
|
||||||
body: "管理员已完成订阅风控复核,你的账户操作限制已解除。",
|
title: "订阅风控限制已解除",
|
||||||
link: "/subscriptions",
|
body: "管理员已完成订阅风控复核,你的账户操作限制已解除。",
|
||||||
dedupeKey: `risk:restriction-restored:${event.id}`,
|
link: "/subscriptions",
|
||||||
});
|
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) {
|
||||||
await notifyUserWithRiskReport({ eventId: event.id, userId: event.userId });
|
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({
|
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 };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
});
|
});
|
||||||
toast.success(dialog.action === "RESTORE_ACCESS" ? "已解除限制" : "已保持限制并记录处置");
|
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);
|
setDialog(null);
|
||||||
router.refresh();
|
router.refresh();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
Reference in New Issue
Block a user