import { useEffect, useMemo, useRef, useState } from "react"; import { App, Button, Drawer, Form, Input, InputNumber, Modal, Select } from "antd"; import type { ColumnsType } from "antd/es/table"; import { BadgeCheck, Coins, Plus, RefreshCw, Search, Trash2, Undo2 } from "lucide-react"; import { PaginationBar } from "@/components/layout/workspace-page"; import { formatCredits } from "@/constant/credits"; import { useDebouncedValue } from "@/hooks/use-debounced-value"; import { listAdminUsers, type AdminReferenceData, type AdminUser } from "@/services/api/auth"; import { adjustAdminUserCredits, getAdminCreditPolicy, listAdminBillingOrders, resolveAdminBillingOrder, resolveAdminBillingOrders, updateAdminCreditPolicy, type BillingOrder } from "@/services/api/wallet"; import { AdminBatchBar, AdminDataTable, AdminRowActions, AdminStatusBadge, AdminTableEmpty } from "./admin-ui"; export type CreditOperation = "policy" | "adjustment" | null; type AdjustmentFormValues = { userId: string; amount: number; note: string }; type ResolutionFormValues = { note: string }; type PolicyMultiplierRow = { model?: string; multiplier?: number }; type PolicyFormValues = { signupBonus: number; checkinBonus: number; defaultMultiplier: number; modelMultipliers: PolicyMultiplierRow[] }; type BillingResolutionAction = "settle" | "refund"; type AdjustmentUser = AdminReferenceData["users"][number] & Partial>; type BillingResolutionTarget = { kind: "single"; order: BillingOrder; action: BillingResolutionAction } | { kind: "batch"; orders: BillingOrder[]; action: BillingResolutionAction }; const billingStatusLabels = { uncertain: "待核对", running: "运行中", reserved: "已预授权", settled: "已结算", refunded: "已退款", } satisfies Record; export default function CreditOperationsPanel({ users, activeOperation, onOperationChange }: { users: AdminReferenceData["users"]; activeOperation: CreditOperation; onOperationChange: (operation: CreditOperation) => void }) { const { message } = App.useApp(); const [orders, setOrders] = useState([]); const [loading, setLoading] = useState(true); const [loadingPolicy, setLoadingPolicy] = useState(false); const [savingPolicy, setSavingPolicy] = useState(false); const [adjusting, setAdjusting] = useState(false); const [resolving, setResolving] = useState(false); const [keyword, setKeyword] = useState(""); const debouncedKeyword = useDebouncedValue(keyword); const [orderStatus, setOrderStatus] = useState<"review" | "all" | BillingOrder["status"]>("review"); const [page, setPage] = useState(1); const [pageSize, setPageSize] = useState(20); const [total, setTotal] = useState(0); const [adjustmentUsers, setAdjustmentUsers] = useState(users); const [adjustmentSearch, setAdjustmentSearch] = useState(""); const debouncedAdjustmentSearch = useDebouncedValue(adjustmentSearch); const [searchingUsers, setSearchingUsers] = useState(false); const [pendingAdjustment, setPendingAdjustment] = useState(null); const [selectedOrderIds, setSelectedOrderIds] = useState([]); const [resolutionTarget, setResolutionTarget] = useState(null); const [adjustmentForm] = Form.useForm(); const [resolutionForm] = Form.useForm(); const [policyForm] = Form.useForm(); const ordersRequestRef = useRef(0); const userSearchRequestRef = useRef(0); const selectedAdjustmentUserId = Form.useWatch("userId", adjustmentForm); const userLabels = useMemo(() => { const labels = new Map(); for (const user of [...users, ...adjustmentUsers]) labels.set(user.id, user.displayName || user.username); return labels; }, [adjustmentUsers, users]); const selectedAdjustmentUser = adjustmentUsers.find((user) => user.id === selectedAdjustmentUserId); const pendingAdjustmentUser = adjustmentUsers.find((user) => user.id === pendingAdjustment?.userId); const reload = async (targetPage = page, targetPageSize = pageSize) => { const requestId = ++ordersRequestRef.current; setLoading(true); try { const result = await listAdminBillingOrders({ keyword: debouncedKeyword || undefined, status: orderStatus, page: targetPage, pageSize: targetPageSize, }); if (requestId !== ordersRequestRef.current) return; if (targetPage > 1 && result.total > 0 && result.orders.length === 0) { setPage(1); return; } setOrders(result.orders); setTotal(result.total); setSelectedOrderIds([]); } catch (error) { if (requestId === ordersRequestRef.current) message.error(error instanceof Error ? error.message : "读取待核对计费失败"); } finally { if (requestId === ordersRequestRef.current) setLoading(false); } }; useEffect(() => { void reload(page, pageSize); }, [debouncedKeyword, orderStatus, page, pageSize]); useEffect(() => { if (activeOperation !== "policy") return; let active = true; setLoadingPolicy(true); void getAdminCreditPolicy() .then(({ policy }) => { if (!active) return; policyForm.setFieldsValue({ signupBonus: policy.signupBonusMicrocredits / 1_000_000, checkinBonus: policy.checkinBonusMicrocredits / 1_000_000, defaultMultiplier: policy.defaultMultiplierBasisPoints / 10_000, modelMultipliers: Object.entries(policy.modelMultiplierBasisPoints).map(([model, value]) => ({ model, multiplier: value / 10_000, })), }); }) .catch((error) => { if (active) message.error(error instanceof Error ? error.message : "读取积分策略失败"); }) .finally(() => { if (active) setLoadingPolicy(false); }); return () => { active = false; }; }, [activeOperation, message, policyForm]); useEffect(() => { if (activeOperation !== "adjustment") return; adjustmentForm.resetFields(); setPendingAdjustment(null); setAdjustmentSearch(""); setAdjustmentUsers(users); }, [activeOperation, adjustmentForm, users]); useEffect(() => { if (activeOperation !== "adjustment") return; const requestId = ++userSearchRequestRef.current; setSearchingUsers(true); void listAdminUsers({ keyword: debouncedAdjustmentSearch.trim() || undefined, page: 1, pageSize: 50 }) .then((result) => { if (requestId !== userSearchRequestRef.current) return; const selectedId = adjustmentForm.getFieldValue("userId"); setAdjustmentUsers((current) => { const selected = current.find((user) => user.id === selectedId); if (selected && !result.users.some((user) => user.id === selected.id)) return [selected, ...result.users]; return result.users; }); }) .catch((error) => { if (requestId === userSearchRequestRef.current) message.error(error instanceof Error ? error.message : "搜索用户失败"); }) .finally(() => { if (requestId === userSearchRequestRef.current) setSearchingUsers(false); }); }, [activeOperation, adjustmentForm, debouncedAdjustmentSearch, message]); const savePolicy = async (values: PolicyFormValues) => { const modelMultiplierBasisPoints: Record = {}; for (const row of values.modelMultipliers || []) { const model = String(row.model || "").trim(); const multiplier = Number(row.multiplier); if (!model || !Number.isFinite(multiplier) || multiplier < 0.0001 || multiplier > 100) { message.error("请完整填写模型标识与 0.0001–100 之间的倍率"); return; } if (model in modelMultiplierBasisPoints) { message.error(`模型“${model}”配置了重复倍率`); return; } modelMultiplierBasisPoints[model] = Math.round(multiplier * 10_000); } setSavingPolicy(true); try { await updateAdminCreditPolicy({ signupBonusMicrocredits: toMicrocredits(values.signupBonus), checkinBonusMicrocredits: toMicrocredits(values.checkinBonus), defaultMultiplierBasisPoints: Math.round(values.defaultMultiplier * 10_000), modelMultiplierBasisPoints, }); message.success("积分策略已保存,将应用于后续创建的计费订单"); onOperationChange(null); } catch (error) { message.error(error instanceof Error ? error.message : "保存积分策略失败"); } finally { setSavingPolicy(false); } }; const previewAdjustment = (values: AdjustmentFormValues) => { const amount = Number(values.amount); if (!Number.isFinite(amount) || amount === 0) { message.error("积分变化不能为 0"); return; } try { toMicrocredits(amount); } catch (error) { message.error(error instanceof Error ? error.message : "积分变化超出可处理范围"); return; } const user = adjustmentUsers.find((item) => item.id === values.userId); if (amount < 0 && hasCreditBalance(user) && user.availableMicrocredits + toMicrocredits(amount) < 0) { message.error("扣减后可用积分不能低于 0"); return; } setPendingAdjustment({ ...values, amount, note: values.note.trim() }); }; const applyAdjustment = async () => { if (!pendingAdjustment) return; setAdjusting(true); try { const result = await adjustAdminUserCredits(pendingAdjustment.userId, { amountMicrocredits: toMicrocredits(pendingAdjustment.amount), note: pendingAdjustment.note, }); setAdjustmentUsers((current) => current.map((user) => user.id === result.account.userId ? { ...user, availableMicrocredits: result.account.availableMicrocredits, reservedMicrocredits: result.account.reservedMicrocredits, } : user, ), ); adjustmentForm.resetFields(); setPendingAdjustment(null); onOperationChange(null); message.success(`用户积分已调整,当前可用积分 ${formatCredits(result.account.availableMicrocredits)}`); } catch (error) { message.error(error instanceof Error ? error.message : "调整积分失败"); } finally { setAdjusting(false); } }; const resolveBilling = async () => { if (!resolutionTarget) return; let values: ResolutionFormValues; try { values = await resolutionForm.validateFields(); } catch { return; } const note = values.note.trim(); setResolving(true); try { if (resolutionTarget.kind === "single") { await resolveAdminBillingOrder(resolutionTarget.order.id, { action: resolutionTarget.action, note }); message.success(resolutionTarget.action === "settle" ? "计费订单已结算" : "预授权积分已退回"); } else { const result = await resolveAdminBillingOrders({ ids: resolutionTarget.orders.map((order) => order.id), action: resolutionTarget.action, note, }); if (result.failed.length > 0) { const failedIds = new Set(result.failed.map((item) => item.id)); const failedOrders = resolutionTarget.orders.filter((order) => failedIds.has(order.id)); const detail = result.failed[0]?.message ? `:${result.failed[0].message}` : ""; if (result.resolvedCount > 0) message.warning(`已处理 ${result.resolvedCount} 条,仍有 ${result.failed.length} 条失败${detail}`); else message.error(`所选 ${result.failed.length} 条订单均处理失败${detail}`); setResolutionTarget({ ...resolutionTarget, orders: failedOrders.length > 0 ? failedOrders : resolutionTarget.orders }); await reload(page, pageSize); return; } message.success(resolutionTarget.action === "settle" ? `已结算 ${result.resolvedCount} 条订单` : `已退回 ${result.resolvedCount} 条订单的预授权积分`); } setResolutionTarget(null); resolutionForm.resetFields(); await reload(page, pageSize); } catch (error) { message.error(error instanceof Error ? error.message : "处理计费订单失败"); } finally { setResolving(false); } }; const openSingleResolution = (order: BillingOrder, action: BillingResolutionAction) => { setResolutionTarget({ kind: "single", order, action }); resolutionForm.resetFields(); }; const openBatchResolution = (action: BillingResolutionAction) => { const selectedOrders = orders.filter((order) => selectedOrderIds.includes(order.id) && canResolveBillingOrder(order)); if (selectedOrders.length === 0) return; setResolutionTarget({ kind: "batch", orders: selectedOrders, action }); resolutionForm.resetFields(); }; const columns: ColumnsType = [ { title: "创建时间", dataIndex: "createdAt", width: 170, align: "center", render: formatTime }, { title: "用户", dataIndex: "userId", width: 150, render: (id: string) => (
{userLabels.get(id) || "未知用户"}
{id}
), }, { title: "模型 / 场景", width: 220, render: (_, order) => (
{order.model}
{order.scene || order.capability}
), }, { title: "预授权积分", width: 125, align: "center", render: (_, order) => {formatCredits(getReservedAmount(order))}, }, { title: "实际结算 / 用量", width: 205, align: "center", render: (_, order) => order.billingMode === "token" ? (
{order.status === "settled" ? `${formatCredits(order.actualAmountMicrocredits)} 积分` : "等待用量结算"}
输入 {order.inputTokens} · 输出 {order.outputTokens} · 缓存 {order.cachedTokens}
) : ( {order.status === "settled" ? formatCredits(order.actualAmountMicrocredits) : "--"} ), }, { title: "状态", dataIndex: "status", width: 110, align: "center", render: (value: BillingOrder["status"]) => , }, { title: "上游请求", dataIndex: "providerRequestId", width: 180, ellipsis: true, render: (value) => value || "未获取" }, { title: "核对原因", dataIndex: "error", width: 260, ellipsis: true, render: (value) => value || "费用状态不明确" }, { title: "操作", width: 190, align: "center", fixed: "right", render: (_, order) => !canResolveBillingOrder(order) ? ( 处理完成 ) : (
openSingleResolution(order, "settle") }} actions={[{ key: "refund", label: "退回预授权", danger: true, onClick: () => openSingleResolution(order, "refund") }]} />
), }, ]; const hasFilters = Boolean(keyword || orderStatus !== "review"); const resolutionOrders = resolutionTarget?.kind === "batch" ? resolutionTarget.orders : resolutionTarget ? [resolutionTarget.order] : []; const resolutionReservedTotal = resolutionOrders.reduce((sum, order) => sum + getReservedAmount(order), 0); const resolutionTitle = resolutionTarget ? (resolutionTarget.action === "settle" ? (resolutionTarget.kind === "batch" ? "批量确认结算" : "确认结算计费订单") : resolutionTarget.kind === "batch" ? "批量退回预授权积分" : "确认退回预授权积分") : ""; return (

异常计费核对

} value={keyword} placeholder="搜索用户、模型、场景或请求号" onChange={(event) => { setKeyword(event.target.value); setPage(1); }} /> } toolbarActive={hasFilters} toolbarFilters={
))} ) : (
暂无模型独立倍率,所有模型使用默认倍率。
)} )} )} { if (adjusting || pendingAdjustment) return; onOperationChange(null); }} rootClassName="admin-drawer admin-credit-drawer" destroyOnHidden mask={{ closable: !adjusting && !pendingAdjustment }} keyboard={!adjusting && !pendingAdjustment} footer={
} >
账务写入操作

提交后会立即写入积分流水和管理员审计记录,请填写可追溯的处理依据。