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 && } onClick={openProductCreate}>新增商品}
+
+
+
+ )
+
+ const orderContent = (
+
+
+ 发货链接由后端生成并带签名,用户只填写游戏 UID。
+ {canManage && (
+
+ } onClick={openTestOrderCreate}>
+ 创建测试订单
+
+
+ )}
+
+
+
+ )
+
+ const walletContent = (
+
+
+
+ {money(wallet?.available_balance ?? 0)}
+ {money(wallet?.frozen_balance ?? 0)}
+ {wallet?.currency || 'POINT'}
+
+ {canFinance && (
+ }
+ onClick={() => {
+ walletForm.resetFields()
+ walletForm.setFieldsValue({ idempotency_key: `manual-${Date.now()}` })
+ setWalletOpen(true)
+ }}
+ >
+ 调整
+
+ )}
+
+ {canFinance ? (
+
+ ) : (
+ 当前角色可查看余额,钱包流水仅财务或负责人可见。
+ )}
+
+ )
+
+ const apiKeyContent = (
+
+
+ 用于开放 API 调用鉴权。新 Secret 只在创建后显示一次,请及时保存。
+ {canManage && } onClick={() => {
+ apiClientForm.resetFields()
+ apiClientForm.setFieldsValue({ signature_version: 'v1', scopes: ['products:read', 'orders:read', 'orders:write'] })
+ setApiClientOpen(true)
+ }}>新增密钥}
+
+
+
+ )
+
+ const callbackContent = (
+
+
+ 回调事件通过 outbox 持久化发送。
+ {canManage && } onClick={() => {
+ callbackForm.resetFields()
+ callbackForm.setFieldsValue({ events: ['order.fulfillment.updated'] })
+ setCallbackOpen(true)
+ }}>新增回调}
+
+
+
+ )
+
+ const memberContent = (
+
+
+ 成员角色独立于平台账号角色。
+ {merchantRole === 'owner' && } onClick={() => {
+ memberForm.resetFields()
+ memberForm.setFieldsValue({ role: 'operator' })
+ setMemberOpen(true)
+ }}>添加成员}
+
+
+
+ )
+
+ 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 && } onClick={openProductCreate}>新增商品}
-
-
-
- ),
- },
- {
- key: 'orders',
- label: '履约订单',
- disabled: !hasFeature('orders'),
- children: (
-
-
- 发货链接由后端生成并带签名,用户只填写游戏 UID。
- {canManage && (
-
- } onClick={openTestOrderCreate}>
- 创建测试订单
-
-
- )}
-
-
-
- ),
- },
- {
- key: 'wallet',
- label: '钱包',
- disabled: !hasFeature('wallet'),
- children: (
-
-
-
- {money(wallet?.available_balance ?? 0)}
- {money(wallet?.frozen_balance ?? 0)}
- {wallet?.currency || 'POINT'}
-
- {canFinance && (
- }
- onClick={() => {
- walletForm.resetFields()
- walletForm.setFieldsValue({ idempotency_key: `manual-${Date.now()}` })
- setWalletOpen(true)
- }}
- >
- 调整
-
- )}
-
- {canFinance ? (
-
- ) : (
- 当前角色可查看余额,钱包流水仅财务或负责人可见。
- )}
-
- ),
- },
- {
- key: 'api',
- label: 'API 客户端',
- disabled: !hasFeature('api'),
- children: (
-
-
- 新密钥只在创建后显示一次。
- {canManage && } onClick={() => {
- apiClientForm.resetFields()
- apiClientForm.setFieldsValue({ signature_version: 'v1', scopes: ['products:read', 'orders:read', 'orders:write'] })
- setApiClientOpen(true)
- }}>新增客户端}
-
-
-
- ),
- },
- {
- key: 'callbacks',
- label: '回调',
- disabled: !hasFeature('callbacks'),
- children: (
-
-
- 回调事件通过 outbox 持久化发送。
- {canManage && } onClick={() => {
- callbackForm.resetFields()
- callbackForm.setFieldsValue({ events: ['order.fulfillment.updated'] })
- setCallbackOpen(true)
- }}>新增回调}
-
-
-
- ),
- },
- {
- key: 'members',
- label: '成员',
- children: (
-
-
- 成员角色独立于平台账号角色。
- {merchantRole === 'owner' && } onClick={() => {
- memberForm.resetFields()
- memberForm.setFieldsValue({ role: 'operator' })
- setMemberOpen(true)
- }}>添加成员}
-
-
-
- ),
- },
- ]}
- />
+ {fixedTab ? fixedTabContent : (
+ setActiveTab(key as MerchantCenterTab)}
+ items={tabItems}
+ />
+ )}
setProductOpen(false)} destroyOnClose width={620}>
- 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={
- - 在「商户中心 / API 客户端」创建凭证,获得 AppKey 与 AppSecret。
+ - 在「开放 API / API 密钥」创建凭证,获得 AppKey 与 AppSecret。
- 每次请求携带 X-App-Key / X-Timestamp / X-Nonce / X-Sign 四个鉴权头。
- 调用「商品列表」拿到可售 sku,调用「下单」创建订单并扣款。
- 拿到 order_no 后,可选择返回发货链接,或者用结构化发货接口完成自建页面流程。