import { useCallback, useEffect, useMemo, useState } from 'react' import { useLocation } from 'react-router-dom' import { Button, Card, Descriptions, Form, Input, InputNumber, Modal, Popconfirm, Select, Space, Table, Tabs, Tag, Typography, message, } from 'antd' import { ApiOutlined, CopyOutlined, LinkOutlined, PlusOutlined, ReloadOutlined, WalletOutlined, } from '@ant-design/icons' import type { ColumnsType } from 'antd/es/table' import { merchantApi } from '../api' import { formatDateTime } from '../utils/time' import type { ApiClient, ApiCredential, CallbackCredential, CallbackSubscription, CreateTestOrderResult, FulfillmentOrder, Merchant, MerchantMember, MerchantProduct, PageResult, WalletAccount, WalletLedgerEntry, } from '../types' const orderStatusMap: Record = { paid: { color: 'blue', text: '待发货' }, delivering: { color: 'cyan', text: '发货中' }, delivered: { color: 'green', text: '已交付' }, ship_failed: { color: 'red', text: '发货失败' }, cancelled: { color: 'default', text: '已取消' }, } const memberRoleOptions = [ { value: 'owner', label: '负责人' }, { value: 'operator', label: '运营' }, { value: 'finance', label: '财务' }, { value: 'viewer', label: '只读' }, ] const apiClientMax = 5 const scopeOptions = [ { value: 'products:read', label: '商品读取' }, { value: 'orders:read', label: '订单读取' }, { value: 'orders:write', label: '订单写入' }, { value: 'shipping:read', label: '发货读取' }, { value: 'wallet:read', label: '钱包读取' }, ] const eventOptions = [ { value: 'order.created', label: '订单创建' }, { value: 'order.shipping.updated', label: '发货更新' }, { value: 'order.cancelled', label: '订单取消' }, ] const testOrderStatusOptions = [ { value: 'paid', label: '已支付,可发货' }, { value: 'ship_failed', label: '发货失败,可重试' }, ] type MerchantCenterTab = 'products' | 'orders' | 'wallet' | 'api' | 'callbacks' | 'members' interface MerchantCenterProps { fixedTab?: MerchantCenterTab title?: string } export default function MerchantCenter({ fixedTab, title = '商户中心' }: MerchantCenterProps = {}) { const location = useLocation() const routeTab = useMemo(() => fixedTab ?? tabFromSearch(location.search), [fixedTab, location.search]) const [merchant, setMerchant] = useState(null) const [merchantRole, setMerchantRole] = useState() const [activeTab, setActiveTab] = useState(routeTab ?? 'products') const [products, setProducts] = useState>({ list: [], total: 0, page: 1, size: 10 }) const [orders, setOrders] = useState>({ list: [], total: 0, page: 1, size: 10 }) const [ledger, setLedger] = useState>({ list: [], total: 0, page: 1, size: 10 }) const [wallet, setWallet] = useState(null) const [apiClients, setApiClients] = useState([]) const [callbacks, setCallbacks] = useState([]) const [members, setMembers] = useState([]) const [loading, setLoading] = useState(false) const [productOpen, setProductOpen] = useState(false) const [editingProduct, setEditingProduct] = useState(null) const [walletOpen, setWalletOpen] = useState(false) const [apiClientOpen, setApiClientOpen] = useState(false) const [apiCredential, setApiCredential] = useState(null) const [callbackOpen, setCallbackOpen] = useState(false) const [callbackCredential, setCallbackCredential] = useState(null) const [memberOpen, setMemberOpen] = useState(false) const [testOrderOpen, setTestOrderOpen] = useState(false) const [testOrderProducts, setTestOrderProducts] = useState([]) const [testOrderProductsLoading, setTestOrderProductsLoading] = useState(false) const [testOrderResult, setTestOrderResult] = useState(null) const [productForm] = Form.useForm() const [walletForm] = Form.useForm() const [walletFilterForm] = Form.useForm() const [apiClientForm] = Form.useForm() const [callbackForm] = Form.useForm() const [memberForm] = Form.useForm() const [testOrderForm] = Form.useForm() const canManage = merchantRole === 'owner' || merchantRole === 'operator' const canFinance = merchantRole === 'owner' || merchantRole === 'finance' const enabledFeatures = useMemo(() => new Set(featuresToList(merchant?.features)), [merchant?.features]) const hasFeature = useCallback((feature: string) => !merchant || enabledFeatures.has(feature), [enabledFeatures, merchant]) const testOrderProductOptions = useMemo(() => testOrderProducts.map((item) => ({ value: item.sku, label: productOptionLabel(item), })), [testOrderProducts]) const copyDeliveryLink = useCallback((orderNo: string) => { merchantApi.getDeliveryLink(orderNo) .then((data) => navigator.clipboard.writeText(data.delivery_url).then(() => message.success('发货链接已复制'))) .catch((e) => message.error(e instanceof Error ? e.message : '复制失败')) }, []) const openDeliveryLink = useCallback((orderNo: string) => { merchantApi.getDeliveryLink(orderNo) .then((data) => window.open(data.delivery_url, '_blank', 'noopener,noreferrer')) .catch((e) => message.error(e instanceof Error ? e.message : '打开失败')) }, []) const patchDeliveryLinkState = useCallback((orderNo: string, patch: Partial) => { setOrders((prev) => ({ ...prev, list: prev.list.map((item) => item.order_no === orderNo ? { ...item, ...patch } : item), })) setTestOrderResult((prev) => prev?.order.order_no === orderNo ? { ...prev, order: { ...prev.order, ...patch }, } : prev) }, []) const loadCurrent = useCallback(async () => { const data = await merchantApi.current() setMerchant(data.merchant) setMerchantRole(data.role) return data }, []) const loadProducts = useCallback(async (page = products.page, size = products.size) => { const data = await merchantApi.products({ page, size }) setProducts(data) }, [products.page, products.size]) const loadOrders = useCallback(async (page = orders.page, size = orders.size) => { const data = await merchantApi.orders({ page, size }) setOrders(data) }, [orders.page, orders.size]) const revokeDeliveryLink = useCallback((orderNo: string) => { merchantApi.revokeDeliveryLink(orderNo) .then(() => { const revokedAt = new Date().toISOString() message.success('发货链接已作废') patchDeliveryLinkState(orderNo, { delivery_link_revoked_at: revokedAt }) }) .catch((e) => message.error(e instanceof Error ? e.message : '作废失败')) }, [patchDeliveryLinkState]) const restoreDeliveryLink = useCallback((orderNo: string) => { merchantApi.restoreDeliveryLink(orderNo) .then((data) => { message.success('发货链接已恢复') patchDeliveryLinkState(orderNo, { delivery_link_expires_at: data.expires_at, delivery_link_revoked_at: null, }) }) .catch((e) => message.error(e instanceof Error ? e.message : '恢复失败')) }, [patchDeliveryLinkState]) const loadWallet = useCallback(async ( page = ledger.page, size = ledger.size, role = merchantRole, filters?: { reference_no?: string; type?: string }, ) => { const walletData = await merchantApi.wallet() setWallet(walletData) if (role === 'owner' || role === 'finance') { const ledgerData = await merchantApi.ledger({ page, size, ...filters }) setLedger(ledgerData) } else { setLedger({ list: [], total: 0, page, size }) } }, [ledger.page, ledger.size, merchantRole]) const loadAPIClients = useCallback(async () => { const clientData = await merchantApi.apiClients() setApiClients(clientData || []) }, []) const loadCallbacks = useCallback(async (role = merchantRole) => { if (role !== 'owner' && role !== 'operator') { setCallbacks([]) return } const callbackData = await merchantApi.callbacks() setCallbacks(callbackData || []) }, [merchantRole]) const loadMembers = useCallback(async (role = merchantRole) => { if (role !== 'owner' && role !== 'operator') { setMembers([]) return } const memberData = await merchantApi.members() setMembers(memberData || []) }, [merchantRole]) const loadActiveTab = useCallback(async (role = merchantRole, tab = activeTab) => { switch (tab) { case 'products': await loadProducts() return case 'orders': await loadOrders() return case 'wallet': await loadWallet(undefined, undefined, role) return case 'api': await loadAPIClients() return case 'callbacks': await loadCallbacks(role) return case 'members': await loadMembers(role) return } }, [activeTab, loadAPIClients, loadCallbacks, loadMembers, loadOrders, loadProducts, loadWallet, merchantRole]) const loadAll = useCallback(async () => { setLoading(true) try { const current = await loadCurrent() const nextTab = fixedTab ?? resolveEnabledTab(routeTab ?? activeTab, current.merchant.features) if (!fixedTab && nextTab !== activeTab) { setActiveTab(nextTab) } await loadActiveTab(current.role, nextTab) } catch (e) { message.error(e instanceof Error ? e.message : '加载失败') } finally { setLoading(false) } }, [activeTab, fixedTab, loadActiveTab, loadCurrent, routeTab]) useEffect(() => { loadAll() }, [loadAll]) useEffect(() => { if (routeTab && activeTab !== routeTab) { setActiveTab(routeTab) } }, [activeTab, routeTab]) const openProductCreate = () => { setEditingProduct(null) productForm.resetFields() productForm.setFieldsValue({ currency: 'POINT', stock: -1, status: 'active', price_amount: 0, cost_amount: 0, }) setProductOpen(true) } const openProductEdit = (record: MerchantProduct) => { setEditingProduct(record) productForm.setFieldsValue({ ...record, }) setProductOpen(true) } const submitProduct = async () => { const values = await productForm.validateFields() try { const payload = { ...values, } if (editingProduct) { await merchantApi.updateProduct(editingProduct.id, payload) message.success('商品已更新') } else { await merchantApi.createProduct(payload) message.success('商品已创建') } setProductOpen(false) loadProducts() } catch (e) { message.error(e instanceof Error ? e.message : '保存失败') } } const submitWalletAdjust = async () => { const values = await walletForm.validateFields() try { const walletData = await merchantApi.adjustWallet({ amount: values.amount, idempotency_key: values.idempotency_key, note: values.note, }) setWallet(walletData) setWalletOpen(false) message.success('钱包已调整') loadWallet() } catch (e) { message.error(e instanceof Error ? e.message : '调整失败') } } const handleWalletFilter = async () => { const values = await walletFilterForm.validateFields() loadWallet(1, ledger.size, merchantRole, { reference_no: values.reference_no || undefined, type: values.type || undefined, }) } const handleWalletFilterReset = () => { walletFilterForm.resetFields() loadWallet(1, ledger.size, merchantRole, {}) } const openTestOrderCreate = () => { testOrderForm.resetFields() testOrderForm.setFieldsValue({ buyer_reference: '测试买家', note: '联调测试订单', order_status: 'paid', }) setTestOrderOpen(true) setTestOrderProductsLoading(true) merchantApi.products({ page: 1, size: 100 }) .then((data) => { const activeProducts = (data.list || []).filter((item) => item.status === 'active') setTestOrderProducts(activeProducts) if (activeProducts.length > 0) { testOrderForm.setFieldsValue({ sku: activeProducts[0].sku }) } else { message.warning('没有可用商品') } }) .catch((e) => { message.error(e instanceof Error ? e.message : '商品加载失败') }) .finally(() => setTestOrderProductsLoading(false)) } const submitTestOrder = async () => { const values = await testOrderForm.validateFields() try { const result = await merchantApi.createTestOrder({ sku: values.sku, buyer_reference: values.buyer_reference, note: values.note, order_status: values.order_status, }) setTestOrderOpen(false) setTestOrderResult(result) message.success('测试订单已创建') loadOrders() } catch (e) { message.error(e instanceof Error ? e.message : '创建失败') } } const submitAPIClient = async () => { const values = await apiClientForm.validateFields() try { const credential = await merchantApi.createApiClient({ name: values.name, scopes: (values.scopes || []).join(','), signature_version: values.signature_version, expires_at: values.expires_at, }) setApiCredential(credential) setApiClientOpen(false) message.success('API 密钥已创建') loadAPIClients() } catch (e) { message.error(e instanceof Error ? e.message : '创建失败') } } const submitCallback = async () => { const values = await callbackForm.validateFields() try { const credential = await merchantApi.createCallback({ name: values.name, url: values.url, events: (values.events || []).join(','), }) setCallbackCredential(credential) setCallbackOpen(false) message.success('回调已创建') loadCallbacks() } catch (e) { message.error(e instanceof Error ? e.message : '创建失败') } } const submitMember = async () => { const values = await memberForm.validateFields() try { await merchantApi.addMember({ user_id: values.user_id, role: values.role, is_default: values.is_default, }) setMemberOpen(false) message.success('成员已添加') loadMembers() } catch (e) { message.error(e instanceof Error ? e.message : '添加失败') } } const toggleAPIClient = async (record: ApiClient) => { try { await merchantApi.updateApiClientStatus(record.id, record.status === 'active' ? 'disabled' : 'active') message.success('状态已更新') loadAPIClients() } catch (e) { message.error(e instanceof Error ? e.message : '更新失败') } } const deleteAPIClient = async (record: ApiClient) => { try { await merchantApi.deleteApiClient(record.id) message.success('密钥已删除') loadAPIClients() } catch (e) { message.error(e instanceof Error ? e.message : '删除失败') } } const toggleCallback = async (record: CallbackSubscription) => { try { await merchantApi.updateCallbackStatus(record.id, record.status === 'active' ? 'disabled' : 'active') message.success('状态已更新') loadCallbacks() } catch (e) { message.error(e instanceof Error ? e.message : '更新失败') } } const productColumns: ColumnsType = [ { title: 'SKU', dataIndex: 'sku', width: 200, ellipsis: true, render: (v) => {v} }, { title: '名称', dataIndex: 'display_name', width: 200, ellipsis: true, render: (_, r) => r.display_name || r.product?.name || '-' }, { title: '目录编码', dataIndex: ['product', 'code'], width: 140, ellipsis: true, render: (_, r) => r.product?.code || '-' }, { title: '售价', dataIndex: 'price_amount', width: 100, render: money }, { title: '成本', dataIndex: 'cost_amount', width: 100, render: money }, { title: '库存', dataIndex: 'stock', width: 80, render: (v) => (v < 0 ? '无限' : v) }, { title: '状态', dataIndex: 'status', width: 80, render: productStatusTag }, { title: '操作', key: 'action', width: 80, render: (_, record) => canManage ? ( ) : '-', }, ] const orderColumns: ColumnsType = [ { title: '平台订单号', dataIndex: 'order_no', width: 190, ellipsis: true, render: (v) => {v}, }, { title: '商户单号', dataIndex: 'client_order_no', width: 140, ellipsis: true }, { title: 'SKU', dataIndex: 'product_sku', width: 130, ellipsis: true, render: (v) => {v}, }, { title: '商品', dataIndex: 'product_name', width: 220, ellipsis: true }, { title: '基础金额', dataIndex: 'base_amount', width: 88, render: money }, { title: '手续费', dataIndex: 'service_fee_amount', width: 82, render: money }, { title: '扣款合计', dataIndex: 'amount', width: 88, render: money }, { title: '状态', dataIndex: 'order_status', width: 92, render: orderStatusTag }, { title: '上游单号', dataIndex: 'provider_order_no', width: 120, ellipsis: true, render: (v) => v || '-' }, { title: '链接有效期', dataIndex: 'delivery_link_expires_at', width: 140, render: (_, record) => record.delivery_link_revoked_at ? 已作废 : formatDateTime(record.delivery_link_expires_at), }, { title: '时间', dataIndex: 'created_at', width: 150, render: formatDateTime }, { title: '发货链接', key: 'delivery_link', width: 150, render: (_, record) => record.delivery_link_revoked_at ? ( ) : ( ), }, ] const ledgerColumns: ColumnsType = [ { title: '流水号', dataIndex: 'entry_no', width: 300, render: (v) => {v} }, { title: '类型', dataIndex: 'type', width: 80, render: ledgerTypeTag }, { title: '金额', dataIndex: 'amount', width: 120, render: moneyWithSign }, { title: '余额', dataIndex: 'balance_after', width: 120, render: money }, { title: '关联单号', dataIndex: 'reference_no', width: 200, ellipsis: true, render: (v) => v ? {v} : '-' }, { title: '备注', dataIndex: 'note', width: 150, ellipsis: { showTitle: false }, render: (v) => {v || '-'} }, { title: '时间', dataIndex: 'created_at', width: 170, render: formatDateTime }, ] const apiClientColumns: ColumnsType = [ { title: '名称', dataIndex: 'name', width: 140, ellipsis: true }, { title: 'App Key', dataIndex: 'app_key', width: 230, render: (v) => {v} }, { title: '签名', dataIndex: 'signature_version', width: 80, render: (v) => {v} }, { title: '权限', dataIndex: 'scopes', width: 250, render: scopesTag }, { title: '状态', dataIndex: 'status', width: 80, render: activeStatusTag }, { title: '最后使用', dataIndex: 'last_used_at', width: 170, render: formatDateTime }, { title: '操作', key: 'action', width: 150, render: (_, record) => canManage ? ( deleteAPIClient(record)} > ) : '-', }, ] const callbackColumns: ColumnsType = [ { title: '名称', dataIndex: 'name', width: 180, ellipsis: true }, { title: 'URL', dataIndex: 'url', ellipsis: true }, { title: '事件', dataIndex: 'events', width: 260, ellipsis: true }, { title: '状态', dataIndex: 'status', width: 90, render: activeStatusTag }, { title: '创建时间', dataIndex: 'created_at', width: 180, render: formatDateTime }, { title: '操作', key: 'action', width: 90, render: (_, record) => canManage ? ( ) : '-', }, ] const memberColumns: ColumnsType = [ { title: '用户', dataIndex: ['user', 'username'], render: (_, r) => r.user?.username || `#${r.user_id}` }, { title: '昵称', dataIndex: ['user', 'nickname'], render: (_, r) => r.user?.nickname || '-' }, { title: '角色', dataIndex: 'role', width: 120, render: memberRoleTag }, { title: '默认', dataIndex: 'is_default', width: 80, render: (v) => (v ? 默认 : '-') }, { title: '状态', dataIndex: 'status', width: 90, render: (v) => (v === 1 ? 启用 : 禁用) }, ] const productContent = ( 通用商品以商户 SKU 对外销售。 {canManage && } ) const orderContent = ( 发货链接由后端生成并带签名,用户只填写游戏 UID。 {canManage && ( )}
) const walletContent = ( {money(wallet?.available_balance ?? 0)} {money(wallet?.frozen_balance ?? 0)} {wallet?.currency || 'POINT'} {canFinance && ( )} {canFinance && (
)} {canFinance ? (
{ const values = walletFilterForm.getFieldsValue() loadWallet(page, size, merchantRole, { reference_no: values.reference_no || undefined, type: values.type || undefined, }) })} /> ) : ( 当前角色可查看余额,钱包流水仅财务或负责人可见。 )} ) const apiKeyContent = ( 用于开放 API 调用鉴权。新 Secret 只在创建后显示一次,请及时保存。每个商户最多 {apiClientMax} 个。 {canManage && }
) const callbackContent = ( 回调事件通过 outbox 持久化发送。 {canManage && }
) const memberContent = ( 成员角色独立于平台账号角色。 {merchantRole === 'owner' && }
) const tabItems = [ { key: 'products', label: '商品', disabled: !hasFeature('products'), children: productContent }, { key: 'orders', label: '发货订单', disabled: !hasFeature('orders'), children: orderContent }, { key: 'wallet', label: '钱包', disabled: !hasFeature('wallet'), children: walletContent }, { key: 'api', label: 'API 密钥', disabled: !hasFeature('api'), children: apiKeyContent }, { key: 'callbacks', label: '回调', disabled: !hasFeature('callbacks'), children: callbackContent }, { key: 'members', label: '成员', children: memberContent }, ] const fixedTabContent = tabItems.find((item) => item.key === fixedTab)?.children return (
{title} {!fixedTab && ( {merchant ? `${merchant.name} / ${merchant.code}` : '加载中'} · {merchantRole ? roleText(merchantRole) : '-'} )}
{fixedTab ? fixedTabContent : ( setActiveTab(key as MerchantCenterTab)} items={tabItems} /> )} setProductOpen(false)} destroyOnClose width={620}>
{!editingProduct && ( <> )} {!editingProduct && ( )}
setTestOrderOpen(false)} destroyOnClose width={560}>
setApiClientOpen(false)} destroyOnClose>
建议不同用途(下单、查询、对账)分别创建密钥,便于独立禁用与审计。每个商户最多 {apiClientMax} 个。
setApiCredential(null)} footer={}> {apiCredential && } setCallbackOpen(false)} destroyOnClose>