"use client"; import { useMemo, useState, useTransition } from "react"; import { useRouter } from "next/navigation"; import type { SubscriptionRiskFinalAction, SubscriptionRiskReviewStatus, } from "@prisma/client"; import { FileText, LockKeyhole, RotateCcw, Send, ShieldCheck, UnlockKeyhole, } from "lucide-react"; import { toast } from "sonner"; import { finalizeSubscriptionRiskDecision, generateSubscriptionRiskReport, sendSubscriptionRiskReport, updateSubscriptionRiskReview, } from "@/actions/admin/subscription-risk"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; import { getErrorMessage } from "@/lib/errors"; interface RiskReviewMode { status: SubscriptionRiskReviewStatus; label: string; title: string; description: string; icon: "ack" | "open"; } const modes: Record<"OPEN" | "ACKNOWLEDGED", RiskReviewMode> = { OPEN: { status: "OPEN", label: "重新打开", title: "重新打开风控事件", description: "事件会回到待处理状态,便于稍后继续跟进。", icon: "open", }, ACKNOWLEDGED: { status: "ACKNOWLEDGED", label: "确认跟进", title: "确认正在处理", description: "适合先记录已看到、正在核查,暂不解除或关闭事件。", icon: "ack", }, }; type DialogState = | { type: "review"; mode: RiskReviewMode } | { type: "final"; action: SubscriptionRiskFinalAction } | { type: "report" } | null; function ModeIcon({ icon }: { icon: RiskReviewMode["icon"] }) { if (icon === "open") return ; return ; } function finalActionCopy(action: SubscriptionRiskFinalAction, restorableSubscriptionCount: number) { if (action === "RESTORE_ACCESS") { return { icon: , label: "解除限制", title: "确认解除风控限制?", description: restorableSubscriptionCount > 0 ? "会恢复可恢复的暂停订阅,并关闭用户端强制通知。" : "会关闭用户端强制通知,并把事件记录为已解除;当前没有可自动恢复的暂停订阅。", confirm: "确认解除", }; } return { icon: , label: "保持封禁/暂停", title: "确认保持封禁或暂停?", description: "订阅和用户限制会维持当前处置,适合确认订阅链接外泄、公共代理滥用或用户无法解释异常访问来源的情况。", confirm: "保持限制", }; } export function SubscriptionRiskReviewActions({ eventId, reviewStatus, canRestoreSubscription = false, restorableSubscriptionCount = 0, riskReport = null, reportSentAt = null, userRestrictionActive = false, finalAction = null, }: { eventId: string; reviewStatus: SubscriptionRiskReviewStatus; canRestoreSubscription?: boolean; restorableSubscriptionCount?: number; riskReport?: string | null; reportSentAt?: Date | string | null; userRestrictionActive?: boolean; finalAction?: SubscriptionRiskFinalAction | null; }) { const router = useRouter(); const [dialog, setDialog] = useState(null); const [note, setNote] = useState(""); const [notifyUser, setNotifyUser] = useState(Boolean(reportSentAt || userRestrictionActive)); const [reportPreview, setReportPreview] = useState(riskReport ?? ""); const [pending, startTransition] = useTransition(); const availableModes = useMemo(() => { return [modes.ACKNOWLEDGED, modes.OPEN].filter((item) => item.status !== reviewStatus); }, [reviewStatus]); const activeFinalCopy = dialog?.type === "final" ? finalActionCopy(dialog.action, restorableSubscriptionCount) : null; function openReviewDialog(mode: RiskReviewMode) { setDialog({ type: "review", mode }); setNote(""); } function openFinalDialog(action: SubscriptionRiskFinalAction) { setDialog({ type: "final", action }); setNote(""); setNotifyUser(action === "KEEP_RESTRICTED" ? true : Boolean(reportSentAt || userRestrictionActive)); } function handleGenerateReport(openAfterGenerate = true) { startTransition(async () => { try { const result = await generateSubscriptionRiskReport(eventId); setReportPreview(result.report); toast.success("风险报告已生成"); if (openAfterGenerate) setDialog({ type: "report" }); router.refresh(); } catch (error) { toast.error(getErrorMessage(error, "生成风险报告失败")); } }); } function handleSendReport() { startTransition(async () => { try { await sendSubscriptionRiskReport(eventId); toast.success("已发送用户端强制通知"); router.refresh(); } catch (error) { toast.error(getErrorMessage(error, "发送用户通知失败")); } }); } function submitReview() { if (dialog?.type !== "review") return; startTransition(async () => { try { await updateSubscriptionRiskReview(eventId, dialog.mode.status, note); toast.success("风控事件已更新"); setDialog(null); router.refresh(); } catch (error) { toast.error(getErrorMessage(error, "更新风控事件失败")); } }); } function submitFinalDecision() { if (dialog?.type !== "final") return; startTransition(async () => { try { await finalizeSubscriptionRiskDecision(eventId, dialog.action, note, { notifyUser: dialog.action === "KEEP_RESTRICTED" && notifyUser, }); toast.success(dialog.action === "RESTORE_ACCESS" ? "已解除限制" : "已保持限制并记录处置"); setDialog(null); router.refresh(); } catch (error) { toast.error(getErrorMessage(error, "保存最终处置失败")); } }); } return ( <>

风险报告

{reportPreview && ( )}

最终处置

{availableModes.length > 0 && (

队列状态

{availableModes.map((item) => ( ))}
)}
!pending && !open && setDialog(null)}> {dialog?.type === "review" && ( <>
{dialog.mode.title} {dialog.mode.description}