import { useCallback, useEffect, useMemo, useState } from 'react' import { useLocation, useNavigate } from 'react-router-dom' import { Button, Form, Input, InputNumber, Modal, Select, Switch, Tabs, Typography, message, } from 'antd' import { ReloadOutlined } from '@ant-design/icons' import { useAuth } from '../store/auth' import { merchantApi } from '../api' import { PageHeader } from '../components/PageHeader' import type { ApiClient, ApiCredential, CallbackCredential, CallbackSubscription, FulfillmentOrder, Merchant, MerchantMember, MerchantRole, MerchantProduct, PageResult, WalletAccount, WalletLedgerEntry, } from '../types' import { apiClientMax, defaultCallbackFormValues, eventsToValue, featuresToList, productOptionLabel, resolveEnabledTab, rolePermissionOptions, roleText, scopeOptions, tabFromSearch, } from './merchantCenterUtils' import type { MerchantCenterProps, MerchantCenterTab } from './merchantCenterUtils' import { ApiKeysTab, CallbacksTab, MembersTab, OrdersTab, ProductsTab, SecretBlock, WalletTab, } from './merchantCenterTabs' export default function MerchantCenter({ fixedTab, title = '商户中心' }: MerchantCenterProps = {}) { const { isAdmin, user } = useAuth() const location = useLocation() const navigate = useNavigate() const routeTab = useMemo(() => fixedTab ?? tabFromSearch(location.search), [fixedTab, location.search]) const [merchant, setMerchant] = useState(null) const [merchantRole, setMerchantRole] = useState() const [merchantPermissions, setMerchantPermissions] = 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 [orderSource, setOrderSource] = useState<'api' | 'manual'>('api') const [ledger, setLedger] = useState>({ list: [], total: 0, page: 1, size: 10 }) const [wallet, setWallet] = useState(null) const [apiClients, setApiClients] = useState([]) const [callback, setCallback] = useState(null) const [members, setMembers] = useState([]) const [roles, setRoles] = useState([]) const [loading, setLoading] = useState(false) const [apiClientOpen, setApiClientOpen] = useState(false) const [apiCredential, setApiCredential] = useState(null) const [callbackCredential, setCallbackCredential] = useState(null) const [memberOpen, setMemberOpen] = useState(false) const [editingMember, setEditingMember] = useState(null) const [roleOpen, setRoleOpen] = useState(false) const [editingRole, setEditingRole] = useState(null) const [manualOrderOpen, setManualOrderOpen] = useState(false) const [manualOrderSubmitting, setManualOrderSubmitting] = useState(false) const [manualOrderProducts, setManualOrderProducts] = useState([]) const [manualOrderProductsLoading, setManualOrderProductsLoading] = useState(false) const [walletFilterForm] = Form.useForm() const [apiClientForm] = Form.useForm() const [callbackForm] = Form.useForm() const [memberForm] = Form.useForm() const [memberEditForm] = Form.useForm() const [roleForm] = Form.useForm() const [manualOrderForm] = Form.useForm() const hasPermission = useCallback((permission: string) => merchantPermissions.includes(permission), [merchantPermissions]) const canManage = hasPermission('products:manage') const canOrderManage = hasPermission('orders:manage') const canAPIManage = hasPermission('api:manage') const canCallbacksManage = hasPermission('callbacks:manage') const canFinance = hasPermission('wallet:ledger') const enabledFeatures = useMemo(() => new Set(featuresToList(merchant?.features)), [merchant?.features]) const hasFeature = useCallback((feature: string) => !merchant || enabledFeatures.has(feature), [enabledFeatures, merchant]) const manualOrderProductOptions = useMemo(() => manualOrderProducts.map((item) => ({ value: item.sku, label: productOptionLabel(item), })), [manualOrderProducts]) 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), })) }, []) 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 loadCurrent = useCallback(async () => { const data = await merchantApi.current() setMerchant(data.merchant) setMerchantRole(data.role) const nextPermissions = [...new Set(data.permissions || [])].sort() setMerchantPermissions((current) => ( current.length === nextPermissions.length && current.every((value, index) => value === nextPermissions[index]) ? current : nextPermissions )) 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, source = orderSource) => { const data = await merchantApi.orders({ page, size, order_source: source }) setOrders(data) }, [orderSource, orders.page, orders.size]) const loadWallet = useCallback(async ( page = ledger.page, size = ledger.size, permissions = merchantPermissions, filters?: { reference_no?: string; type?: string }, ) => { const walletData = await merchantApi.wallet() setWallet(walletData) if (permissions.includes('wallet:ledger')) { const ledgerData = await merchantApi.ledger({ page, size, ...filters }) setLedger(ledgerData) } else { setLedger({ list: [], total: 0, page, size }) } }, [ledger.page, ledger.size, merchantPermissions]) const loadAPIClients = useCallback(async () => { const clientData = await merchantApi.apiClients() setApiClients(clientData || []) }, []) const loadCallbacks = useCallback(async (permissions = merchantPermissions) => { if (!permissions.includes('callbacks:manage')) { setCallback(null) callbackForm.resetFields() return } const callbackData = await merchantApi.callbacks() setCallback(callbackData || null) callbackForm.setFieldsValue(callbackData ? { url: callbackData.url, events: eventsToValue(callbackData.events), status: callbackData.status, } : defaultCallbackFormValues()) }, [callbackForm, merchantPermissions]) const loadMembers = useCallback(async (permissions = merchantPermissions) => { if (!permissions.includes('members:manage')) { setMembers([]) setRoles([]) return } const [memberData, roleData] = await Promise.all([merchantApi.members(), merchantApi.roles()]) setMembers(memberData || []) setRoles(roleData || []) }, [merchantPermissions]) const loadActiveTab = useCallback(async (permissions = merchantPermissions, tab = activeTab) => { switch (tab) { case 'products': await loadProducts() return case 'orders': await loadOrders() return case 'wallet': await loadWallet(undefined, undefined, permissions) return case 'api': await loadAPIClients() return case 'callbacks': await loadCallbacks(permissions) return case 'members': await loadMembers(permissions) return } }, [activeTab, loadAPIClients, loadCallbacks, loadMembers, loadOrders, loadProducts, loadWallet, merchantPermissions]) 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.permissions || [], 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 openProductToggle = (record: MerchantProduct) => { merchantApi.updateProductStatus(record.id, record.status === 'active' ? 'inactive' : 'active') .then(() => { message.success(record.status === 'active' ? '商品已下架,商户无法再下单' : '商品已上架') loadProducts() }) .catch((e) => message.error(e instanceof Error ? e.message : '操作失败')) } const handleWalletFilter = async () => { const values = await walletFilterForm.validateFields() loadWallet(1, ledger.size, merchantPermissions, { reference_no: values.reference_no || undefined, type: values.type || undefined, }) } const handleWalletFilterReset = () => { walletFilterForm.resetFields() loadWallet(1, ledger.size, merchantPermissions, {}) } const handleLoadLedger = useCallback((page: number, size: number) => { const values = walletFilterForm.getFieldsValue() loadWallet(page, size, merchantPermissions, { reference_no: values.reference_no || undefined, type: values.type || undefined, }) }, [loadWallet, merchantPermissions, walletFilterForm]) const openManualOrderCreate = () => { manualOrderForm.resetFields() manualOrderForm.setFieldsValue({ quantity: 1, }) setManualOrderOpen(true) setManualOrderProductsLoading(true) merchantApi.manualOrderProducts() .then((data) => { setManualOrderProducts(data || []) if (data.length > 0) { manualOrderForm.setFieldsValue({ sku: data[0].sku }) } else { message.warning('没有可用商品') } }) .catch((e) => { message.error(e instanceof Error ? e.message : '商品加载失败') }) .finally(() => setManualOrderProductsLoading(false)) } const submitManualOrder = async () => { const values = await manualOrderForm.validateFields() setManualOrderSubmitting(true) try { const result = await merchantApi.createManualOrder({ client_order_no: values.client_order_no, sku: values.sku, quantity: values.quantity, buyer_reference: values.buyer_reference, game_account: values.game_account, note: values.note, }) setManualOrderOpen(false) setOrderSource('manual') message.success(result.idempotent ? '已找到已有手动订单,未重复扣款' : '手动订单已创建,余额与库存已扣减') await loadOrders(1, orders.size, 'manual') } catch (e) { message.error(e instanceof Error ? e.message : '创建失败') } finally { setManualOrderSubmitting(false) } } const changeOrderSource = (source: 'api' | 'manual') => { setOrderSource(source) loadOrders(1, orders.size, source).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.saveCallback({ url: values.url, events: (values.events || []).join(','), status: values.status, }) setCallback(credential.subscription) if (credential.secret) { setCallbackCredential(credential) } message.success('回调配置已保存') } catch (e) { message.error(e instanceof Error ? e.message : '保存失败') } } const rotateCallbackSecret = async () => { const values = await callbackForm.validateFields() try { const credential = await merchantApi.saveCallback({ url: values.url, events: (values.events || []).join(','), status: values.status, rotate_secret: true, }) setCallback(credential.subscription) if (credential.secret) { setCallbackCredential(credential) } message.success('回调密钥已重置,旧密钥立即失效') } catch (e) { message.error(e instanceof Error ? e.message : '重置失败') } } const submitMember = async () => { const values = await memberForm.validateFields() try { await merchantApi.addMember({ username: values.username, password: values.password, nickname: values.nickname, role: values.role, }) setMemberOpen(false) message.success('成员已添加') loadMembers() } catch (e) { message.error(e instanceof Error ? e.message : '添加失败') } } const submitMemberEdit = async () => { if (!editingMember) return try { const values = await memberEditForm.validateFields() await merchantApi.updateMember(editingMember.id, { role: values.role, status: values.status, is_default: values.is_default || false, }) setEditingMember(null) message.success('成员已更新') loadMembers() } catch (e) { message.error(e instanceof Error ? e.message : '更新失败') } } const toggleMemberStatus = async (member: MerchantMember) => { try { await merchantApi.updateMember(member.id, { role: member.role, status: member.status === 1 ? 0 : 1, is_default: member.is_default, }) message.success(member.status === 1 ? '员工已停用' : '员工已启用') loadMembers() } catch (e) { message.error(e instanceof Error ? e.message : '操作失败') } } const removeMember = async (member: MerchantMember) => { try { await merchantApi.removeMember(member.id) message.success('员工已从当前商户移除') loadMembers() } catch (e) { message.error(e instanceof Error ? e.message : '移除失败') } } const submitRole = async () => { const values = await roleForm.validateFields() try { if (editingRole) { await merchantApi.updateRole(editingRole.id, { name: values.name, permissions: values.permissions || [] }) } else { await merchantApi.createRole({ ...values, permissions: values.permissions || [] }) } setRoleOpen(false) setEditingRole(null) message.success(editingRole ? '角色已更新' : '角色已创建') 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 tabItems = [ { key: 'products', label: '商品列表', disabled: !hasFeature('products'), children: ( ) }, { key: 'orders', label: '发货订单', disabled: !hasFeature('orders'), children: ( ) }, { key: 'wallet', label: '钱包', disabled: !hasFeature('wallet'), children: ( ) }, { key: 'api', label: 'API 密钥', disabled: !hasFeature('api'), children: ( setApiClientOpen(true)} onNavigateDocs={() => navigate('/open-api')} onNavigateDebug={() => navigate('/api-debug')} onToggleClient={toggleAPIClient} onDeleteClient={deleteAPIClient} /> ) }, { key: 'callbacks', label: '回调', disabled: !hasFeature('callbacks'), children: ( ) }, { key: 'members', label: '成员', children: ( setMemberOpen(true)} onOpenCreateRole={() => { setEditingRole(null) setRoleOpen(true) }} onEditRole={(role) => { setEditingRole(role) roleForm.setFieldsValue({ name: role.name, code: role.code, permissions: role.permissions.split(',').filter(Boolean), }) setRoleOpen(true) }} onEditMember={(member) => { setEditingMember(member) memberEditForm.setFieldsValue({ role: member.role, status: member.status, is_default: member.is_default }) }} onToggleMemberStatus={toggleMemberStatus} onRemoveMember={removeMember} currentUserID={user?.id} /> ) }, ] const fixedTabContent = tabItems.find((item) => item.key === fixedTab)?.children const parentCategoryMap: Record = { products: '商品管理', orders: '订单管理', wallet: '积分管理', api: '开放 API', callbacks: '开放 API', members: '员工管理', } const currentTabKey = fixedTab ?? activeTab const parentGroup = parentCategoryMap[currentTabKey] const computedBreadcrumbs = useMemo(() => { const pageTitle = title === '商品' ? '商品列表' : title if (parentGroup) { return [{ title: parentGroup }, { title: pageTitle }] } return [{ title: pageTitle }] }, [parentGroup, title]) return (
} loading={loading} onClick={loadAll}> 刷新 } /> {fixedTab ? fixedTabContent : ( setActiveTab(key as MerchantCenterTab)} items={tabItems} /> )} setManualOrderOpen(false)} destroyOnClose width={560}>
setApiClientOpen(false)} destroyOnClose>
建议不同用途(下单、查询、对账)分别创建密钥,便于独立禁用与审计。每个商户最多 {apiClientMax} 个。
setApiCredential(null)} footer={}> {apiCredential && } setCallbackCredential(null)} footer={}> {callbackCredential && } setMemberOpen(false)} destroyOnClose>
{ setRoleOpen(false); setEditingRole(null) }} destroyOnClose>
{!editingRole && }