diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index af6b2d2..065bb8a 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -38,6 +38,12 @@ function AppRoutes() { > } /> } /> + } /> + } /> + } /> + } /> + } /> + } /> , - children: [{ key: 'platform-products', label: '平台授权商品', disabled: true }], + children: [{ key: 'merchant-products', label: '商品', path: '/merchant-products' }], }, { key: 'shops', - label: '店铺管理', + label: '商户管理', icon: , - children: [ - { key: 'shop-list', label: '店铺列表', path: '/platform-merchants' }, - { key: 'shop-products', label: '店铺商品绑定', disabled: true }, - ], + children: [{ key: 'shop-list', label: '商户列表', path: '/platform-merchants' }], }, { key: 'orders', label: '订单管理', icon: , - children: [ - { key: 'auto-orders', label: '自动发货订单', disabled: true }, - { key: 'manual-verify', label: '手动核销/反核销', disabled: true }, - { key: 'after-sales', label: '售后订单', disabled: true }, - { key: 'manual-orders', label: '手动处理订单', disabled: true }, - ], + children: [{ key: 'fulfillment-orders', label: '履约订单', path: '/merchant-orders' }], }, { key: 'staff', label: '员工管理', icon: , - children: [ - { key: 'staff-list', label: '员工列表', disabled: true }, - { key: 'positions', label: '岗位列表', disabled: true }, - { key: 'operation-logs', label: '操作日志', disabled: true }, - ], + children: [{ key: 'merchant-members', label: '成员', path: '/merchant-members' }], }, { key: 'funds', label: '资金', icon: , - children: [ - { key: 'point-details', label: '积分明细', disabled: true }, - { key: 'recharge-requests', label: '充值申请', disabled: true }, - ], + children: [{ key: 'merchant-wallet', label: '钱包', path: '/merchant-wallet' }], }, { key: 'open-api', label: '开放 API', icon: , children: [ - { key: 'api-keys', label: 'API 密钥', path: '/merchant-center' }, + { key: 'api-keys', label: 'API 密钥', path: '/merchant-api-keys' }, + { key: 'api-callbacks', label: '回调', path: '/merchant-callbacks' }, { key: 'api-docs', label: '对接文档', path: '/open-api' }, { key: 'api-debug', label: '调用调试', path: '/api-debug' }, ], @@ -114,17 +100,60 @@ const merchantSections: SidebarSection[] = [ icon: , path: '/', }, + { + key: 'products', + label: '商品管理', + icon: , + children: [{ key: 'merchant-products', label: '商品', path: '/merchant-products' }], + }, + { + key: 'orders', + label: '订单管理', + icon: , + children: [{ key: 'fulfillment-orders', label: '履约订单', path: '/merchant-orders' }], + }, + { + key: 'funds', + label: '资金', + icon: , + children: [{ key: 'merchant-wallet', label: '钱包', path: '/merchant-wallet' }], + }, + { + key: 'staff', + label: '员工管理', + icon: , + children: [{ key: 'merchant-members', label: '成员', path: '/merchant-members' }], + }, { key: 'open-api', label: '开放 API', icon: , - children: [{ key: 'api-keys', label: 'API 密钥', path: '/merchant-center' }], + children: [ + { key: 'api-keys', label: 'API 密钥', path: '/merchant-api-keys' }, + { key: 'api-callbacks', label: '回调', path: '/merchant-callbacks' }, + ], }, ] -function getSelectedKey(pathname: string) { +function getSelectedKey(pathname: string, search: string) { if (pathname === '/') return 'dashboard' - if (pathname.startsWith('/merchant-center')) return 'api-keys' + if (pathname.startsWith('/merchant-products')) return 'merchant-products' + if (pathname.startsWith('/merchant-orders')) return 'fulfillment-orders' + if (pathname.startsWith('/merchant-wallet')) return 'merchant-wallet' + if (pathname.startsWith('/merchant-members')) return 'merchant-members' + if (pathname.startsWith('/merchant-callbacks')) return 'api-callbacks' + if (pathname.startsWith('/merchant-api-keys')) return 'api-keys' + if (pathname.startsWith('/merchant-center')) { + const tab = new URLSearchParams(search).get('tab') + const tabKeys: Record = { + products: 'merchant-products', + orders: 'fulfillment-orders', + wallet: 'merchant-wallet', + members: 'merchant-members', + callbacks: 'api-callbacks', + } + return tab ? tabKeys[tab] || 'merchant-products' : 'merchant-products' + } if (pathname.startsWith('/open-api')) return 'api-docs' if (pathname.startsWith('/api-debug')) return 'api-debug' if (pathname.startsWith('/platform-merchants')) return 'shop-list' @@ -145,7 +174,7 @@ export default function MainLayout() { const isDocsPage = location.pathname.startsWith('/open-api') const sections = useMemo(() => (isAdmin ? adminSections : merchantSections), [isAdmin]) - const selectedKey = getSelectedKey(location.pathname) + const selectedKey = getSelectedKey(location.pathname, location.search) const [openKeys, setOpenKeys] = useState(() => getInitialOpenKeys(sections, selectedKey)) useEffect(() => { diff --git a/frontend/src/pages/ApiDebugger.tsx b/frontend/src/pages/ApiDebugger.tsx index b324b09..0702a84 100644 --- a/frontend/src/pages/ApiDebugger.tsx +++ b/frontend/src/pages/ApiDebugger.tsx @@ -238,7 +238,7 @@ export default function ApiDebugger() { API 调试工具 - 填写商户 API 客户端的 AppKey 与 Secret,选择接口并发送请求。签名在浏览器端使用 Web Crypto API 计算。 + 填写商户 API 密钥的 AppKey 与 Secret,选择接口并发送请求。签名在浏览器端使用 Web Crypto API 计算。 } suffix={`/ ${numberText(stats?.api_client_count)}`} diff --git a/frontend/src/pages/MerchantCenter.tsx b/frontend/src/pages/MerchantCenter.tsx index 0fe831a..49a8469 100644 --- a/frontend/src/pages/MerchantCenter.tsx +++ b/frontend/src/pages/MerchantCenter.tsx @@ -1,4 +1,5 @@ import { useCallback, useEffect, useMemo, useState } from 'react' +import { useLocation } from 'react-router-dom' import { Button, Descriptions, @@ -81,10 +82,19 @@ const testOrderStatusOptions = [ { value: 'failed', label: '发货失败,可重试' }, ] -export default function MerchantCenter() { +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('products') + 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 }) @@ -252,8 +262,8 @@ export default function MerchantCenter() { setLoading(true) try { const current = await loadCurrent() - const nextTab = resolveEnabledTab(activeTab, current.merchant.features) - if (nextTab !== activeTab) { + const nextTab = fixedTab ?? resolveEnabledTab(routeTab ?? activeTab, current.merchant.features) + if (!fixedTab && nextTab !== activeTab) { setActiveTab(nextTab) } await loadActiveTab(current.role, nextTab) @@ -262,12 +272,18 @@ export default function MerchantCenter() { } finally { setLoading(false) } - }, [activeTab, loadActiveTab, loadCurrent]) + }, [activeTab, fixedTab, loadActiveTab, loadCurrent, routeTab]) useEffect(() => { loadAll() }, [loadAll]) + useEffect(() => { + if (routeTab && activeTab !== routeTab) { + setActiveTab(routeTab) + } + }, [activeTab, routeTab]) + const openProductCreate = () => { setEditingProduct(null) productForm.resetFields() @@ -380,7 +396,7 @@ export default function MerchantCenter() { }) setApiCredential(credential) setApiClientOpen(false) - message.success('API 客户端已创建') + message.success('API 密钥已创建') loadAPIClients() } catch (e) { message.error(e instanceof Error ? e.message : '创建失败') @@ -459,27 +475,39 @@ export default function MerchantCenter() { ] const orderColumns: ColumnsType = [ - { title: '平台订单号', dataIndex: 'order_no', width: 210, render: (v) => {v} }, - { title: '商户单号', dataIndex: 'client_order_no', width: 160, ellipsis: true }, - { title: 'SKU', dataIndex: 'product_sku', width: 150, render: (v) => {v} }, - { title: '商品', dataIndex: 'product_name', ellipsis: true }, - { title: '基础金额', dataIndex: 'base_amount', width: 100, render: money }, - { title: '手续费', dataIndex: 'service_fee_amount', width: 100, render: money }, - { title: '扣款合计', dataIndex: 'amount', width: 100, render: money }, - { title: '支付', dataIndex: 'payment_status', width: 90, render: paymentStatusTag }, - { title: '履约', dataIndex: 'fulfillment_status', width: 100, render: fulfillmentStatusTag }, - { title: '上游单号', dataIndex: 'provider_order_no', width: 140, ellipsis: true, render: (v) => v || '-' }, + { + 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: 'payment_status', width: 76, render: paymentStatusTag }, + { title: '履约', dataIndex: 'fulfillment_status', width: 86, render: fulfillmentStatusTag }, + { title: '上游单号', dataIndex: 'provider_order_no', width: 120, ellipsis: true, render: (v) => v || '-' }, { title: '链接有效期', dataIndex: 'delivery_link_expires_at', - width: 160, + width: 140, render: (_, record) => record.delivery_link_revoked_at ? 已作废 : formatDateTime(record.delivery_link_expires_at), }, - { title: '时间', dataIndex: 'created_at', width: 180, render: formatDateTime }, + { title: '时间', dataIndex: 'created_at', width: 150, render: formatDateTime }, { title: '发货链接', key: 'delivery_link', - width: 220, + width: 150, render: (_, record) => record.delivery_link_revoked_at ? ( ) : ( @@ -547,12 +575,141 @@ export default function MerchantCenter() { { 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 ? ( +
+ ) : ( + 当前角色可查看余额,钱包流水仅财务或负责人可见。 + )} + + ) + + const apiKeyContent = ( + + + 用于开放 API 调用鉴权。新 Secret 只在创建后显示一次,请及时保存。 + {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} {merchant ? `${merchant.name} / ${merchant.code}` : '加载中'} · {merchantRole ? roleText(merchantRole) : '-'} @@ -563,154 +720,13 @@ export default function MerchantCenter() { - - - 通用商品以商户 SKU 对外销售。 - {canManage && } - -
- - ), - }, - { - key: 'orders', - label: '履约订单', - disabled: !hasFeature('orders'), - children: ( - - - 发货链接由后端生成并带签名,用户只填写游戏 UID。 - {canManage && ( - - - - )} - -
- - ), - }, - { - key: 'wallet', - label: '钱包', - disabled: !hasFeature('wallet'), - children: ( - - - - {money(wallet?.available_balance ?? 0)} - {money(wallet?.frozen_balance ?? 0)} - {wallet?.currency || 'POINT'} - - {canFinance && ( - - )} - - {canFinance ? ( -
- ) : ( - 当前角色可查看余额,钱包流水仅财务或负责人可见。 - )} - - ), - }, - { - key: 'api', - label: 'API 客户端', - disabled: !hasFeature('api'), - children: ( - - - 新密钥只在创建后显示一次。 - {canManage && } - -
- - ), - }, - { - key: 'callbacks', - label: '回调', - disabled: !hasFeature('callbacks'), - children: ( - - - 回调事件通过 outbox 持久化发送。 - {canManage && } - -
- - ), - }, - { - key: 'members', - label: '成员', - children: ( - - - 成员角色独立于平台账号角色。 - {merchantRole === 'owner' && } - -
- - ), - }, - ]} - /> + {fixedTab ? fixedTabContent : ( + setActiveTab(key as MerchantCenterTab)} + items={tabItems} + /> + )} setProductOpen(false)} destroyOnClose width={620}>
@@ -847,7 +863,7 @@ export default function MerchantCenter() {
- setApiClientOpen(false)} destroyOnClose> + setApiClientOpen(false)} destroyOnClose>
@@ -942,9 +958,15 @@ function featuresToList(features?: string) { return list.length > 0 ? list : ['products', 'orders', 'wallet', 'api', 'callbacks'] } -function resolveEnabledTab(current: string, features?: string) { +function tabFromSearch(search: string): MerchantCenterTab | null { + const tab = new URLSearchParams(search).get('tab') + const allowedTabs: MerchantCenterTab[] = ['products', 'orders', 'wallet', 'api', 'callbacks', 'members'] + return allowedTabs.includes(tab as MerchantCenterTab) ? tab as MerchantCenterTab : null +} + +function resolveEnabledTab(current: MerchantCenterTab, features?: string): MerchantCenterTab { const enabled = new Set(featuresToList(features)) - const tabFeatures: Record = { + const tabFeatures: Record = { products: 'products', orders: 'orders', wallet: 'wallet', @@ -956,7 +978,7 @@ function resolveEnabledTab(current: string, features?: string) { if (feature === null || enabled.has(feature)) { return current } - return ['products', 'orders', 'wallet', 'api', 'callbacks'].find((key) => enabled.has(key)) || 'members' + return (['products', 'orders', 'wallet', 'api', 'callbacks'] as MerchantCenterTab[]).find((key) => enabled.has(key)) || 'members' } function money(value?: number | null) { diff --git a/frontend/src/pages/OpenApiDocs.tsx b/frontend/src/pages/OpenApiDocs.tsx index 81d1925..3aa1a4c 100644 --- a/frontend/src/pages/OpenApiDocs.tsx +++ b/frontend/src/pages/OpenApiDocs.tsx @@ -295,7 +295,7 @@ function OverviewSection() { title="概述" desc={ <> - 面向商户系统调用的开放 API。凭证在「商户中心 / API 客户端」创建, + 面向商户系统调用的开放 API。凭证在「开放 API / API 密钥」创建, 采用 X-App-Key + HMAC-SHA256 签名鉴权。 发货链路同时支持「返回签名链接」和「返回结构化数据」两种模式。 /api/open/v1 仍是上游回调链路,和本页的商户 API 分开。 @@ -309,7 +309,7 @@ function OverviewSection() { message="一句话上手" description={
    -
  1. 在「商户中心 / API 客户端」创建凭证,获得 AppKeyAppSecret
  2. +
  3. 在「开放 API / API 密钥」创建凭证,获得 AppKeyAppSecret
  4. 每次请求携带 X-App-Key / X-Timestamp / X-Nonce / X-Sign 四个鉴权头。
  5. 调用「商品列表」拿到可售 sku,调用「下单」创建订单并扣款。
  6. 拿到 order_no 后,可选择返回发货链接,或者用结构化发货接口完成自建页面流程。