feat: add manual fulfillment order creation
This commit is contained in:
@@ -2,33 +2,24 @@ import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { useLocation, useNavigate } from 'react-router-dom'
|
||||
import {
|
||||
Button,
|
||||
Descriptions,
|
||||
Form,
|
||||
Input,
|
||||
InputNumber,
|
||||
Modal,
|
||||
Select,
|
||||
Space,
|
||||
Tabs,
|
||||
Tag,
|
||||
Typography,
|
||||
message,
|
||||
} from 'antd'
|
||||
import {
|
||||
CopyOutlined,
|
||||
LinkOutlined,
|
||||
ReloadOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import { ReloadOutlined } from '@ant-design/icons'
|
||||
import { useAuth } from '../store/auth'
|
||||
import { merchantApi } from '../api'
|
||||
import { formatDateTime } from '../utils/time'
|
||||
import { PageHeader } from '../components/PageHeader'
|
||||
import type {
|
||||
ApiClient,
|
||||
ApiCredential,
|
||||
CallbackCredential,
|
||||
CallbackSubscription,
|
||||
CreateTestOrderResult,
|
||||
FulfillmentOrder,
|
||||
Merchant,
|
||||
MerchantMember,
|
||||
@@ -48,7 +39,6 @@ import {
|
||||
roleText,
|
||||
scopeOptions,
|
||||
tabFromSearch,
|
||||
testOrderStatusOptions,
|
||||
} from './merchantCenterUtils'
|
||||
import type { MerchantCenterProps, MerchantCenterTab } from './merchantCenterUtils'
|
||||
import {
|
||||
@@ -71,6 +61,7 @@ export default function MerchantCenter({ fixedTab, title = '商户中心' }: Mer
|
||||
const [activeTab, setActiveTab] = useState<MerchantCenterTab>(routeTab ?? 'products')
|
||||
const [products, setProducts] = useState<PageResult<MerchantProduct>>({ list: [], total: 0, page: 1, size: 10 })
|
||||
const [orders, setOrders] = useState<PageResult<FulfillmentOrder>>({ list: [], total: 0, page: 1, size: 10 })
|
||||
const [orderSource, setOrderSource] = useState<'api' | 'manual'>('api')
|
||||
const [ledger, setLedger] = useState<PageResult<WalletLedgerEntry>>({ list: [], total: 0, page: 1, size: 10 })
|
||||
const [wallet, setWallet] = useState<WalletAccount | null>(null)
|
||||
const [apiClients, setApiClients] = useState<ApiClient[]>([])
|
||||
@@ -81,24 +72,24 @@ export default function MerchantCenter({ fixedTab, title = '商户中心' }: Mer
|
||||
const [apiCredential, setApiCredential] = useState<ApiCredential | null>(null)
|
||||
const [callbackCredential, setCallbackCredential] = useState<CallbackCredential | null>(null)
|
||||
const [memberOpen, setMemberOpen] = useState(false)
|
||||
const [testOrderOpen, setTestOrderOpen] = useState(false)
|
||||
const [testOrderProducts, setTestOrderProducts] = useState<MerchantProduct[]>([])
|
||||
const [testOrderProductsLoading, setTestOrderProductsLoading] = useState(false)
|
||||
const [testOrderResult, setTestOrderResult] = useState<CreateTestOrderResult | null>(null)
|
||||
const [manualOrderOpen, setManualOrderOpen] = useState(false)
|
||||
const [manualOrderSubmitting, setManualOrderSubmitting] = useState(false)
|
||||
const [manualOrderProducts, setManualOrderProducts] = useState<MerchantProduct[]>([])
|
||||
const [manualOrderProductsLoading, setManualOrderProductsLoading] = useState(false)
|
||||
const [walletFilterForm] = Form.useForm()
|
||||
const [apiClientForm] = Form.useForm()
|
||||
const [callbackForm] = Form.useForm()
|
||||
const [memberForm] = Form.useForm()
|
||||
const [testOrderForm] = Form.useForm()
|
||||
const [manualOrderForm] = 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) => ({
|
||||
const manualOrderProductOptions = useMemo(() => manualOrderProducts.map((item) => ({
|
||||
value: item.sku,
|
||||
label: productOptionLabel(item),
|
||||
})), [testOrderProducts])
|
||||
})), [manualOrderProducts])
|
||||
|
||||
const copyDeliveryLink = useCallback((orderNo: string) => {
|
||||
merchantApi.getDeliveryLink(orderNo)
|
||||
@@ -117,10 +108,6 @@ export default function MerchantCenter({ fixedTab, title = '商户中心' }: Mer
|
||||
...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 revokeDeliveryLink = useCallback((orderNo: string) => {
|
||||
@@ -157,10 +144,10 @@ export default function MerchantCenter({ fixedTab, title = '商户中心' }: Mer
|
||||
setProducts(data)
|
||||
}, [products.page, products.size])
|
||||
|
||||
const loadOrders = useCallback(async (page = orders.page, size = orders.size) => {
|
||||
const data = await merchantApi.orders({ page, 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)
|
||||
}, [orders.page, orders.size])
|
||||
}, [orderSource, orders.page, orders.size])
|
||||
|
||||
const loadWallet = useCallback(async (
|
||||
page = ledger.page,
|
||||
@@ -286,21 +273,25 @@ export default function MerchantCenter({ fixedTab, title = '商户中心' }: Mer
|
||||
})
|
||||
}, [loadWallet, merchantRole, walletFilterForm])
|
||||
|
||||
const openTestOrderCreate = () => {
|
||||
testOrderForm.resetFields()
|
||||
testOrderForm.setFieldsValue({
|
||||
buyer_reference: '测试买家',
|
||||
note: '联调测试订单',
|
||||
order_status: 'paid',
|
||||
const newManualClientOrderNo = () => {
|
||||
const suffix = window.crypto?.randomUUID?.().replaceAll('-', '') || `${Date.now()}${Math.random().toString(36).slice(2)}`
|
||||
return `manual-${suffix.slice(0, 32)}`
|
||||
}
|
||||
|
||||
const openManualOrderCreate = () => {
|
||||
manualOrderForm.resetFields()
|
||||
manualOrderForm.setFieldsValue({
|
||||
client_order_no: newManualClientOrderNo(),
|
||||
quantity: 1,
|
||||
})
|
||||
setTestOrderOpen(true)
|
||||
setTestOrderProductsLoading(true)
|
||||
setManualOrderOpen(true)
|
||||
setManualOrderProductsLoading(true)
|
||||
merchantApi.products({ page: 1, size: 100 })
|
||||
.then((data) => {
|
||||
const activeProducts = (data.list || []).filter((item) => item.status === 'active')
|
||||
setTestOrderProducts(activeProducts)
|
||||
setManualOrderProducts(activeProducts)
|
||||
if (activeProducts.length > 0) {
|
||||
testOrderForm.setFieldsValue({ sku: activeProducts[0].sku })
|
||||
manualOrderForm.setFieldsValue({ sku: activeProducts[0].sku })
|
||||
} else {
|
||||
message.warning('没有可用商品')
|
||||
}
|
||||
@@ -308,27 +299,37 @@ export default function MerchantCenter({ fixedTab, title = '商户中心' }: Mer
|
||||
.catch((e) => {
|
||||
message.error(e instanceof Error ? e.message : '商品加载失败')
|
||||
})
|
||||
.finally(() => setTestOrderProductsLoading(false))
|
||||
.finally(() => setManualOrderProductsLoading(false))
|
||||
}
|
||||
|
||||
const submitTestOrder = async () => {
|
||||
const values = await testOrderForm.validateFields()
|
||||
const submitManualOrder = async () => {
|
||||
const values = await manualOrderForm.validateFields()
|
||||
setManualOrderSubmitting(true)
|
||||
try {
|
||||
const result = await merchantApi.createTestOrder({
|
||||
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,
|
||||
order_status: values.order_status,
|
||||
})
|
||||
setTestOrderOpen(false)
|
||||
setTestOrderResult(result)
|
||||
message.success('测试订单已创建')
|
||||
loadOrders()
|
||||
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 {
|
||||
@@ -425,7 +426,7 @@ export default function MerchantCenter({ fixedTab, title = '商户中心' }: Mer
|
||||
<ProductsTab loading={loading} products={products} canManage={canManage} onToggleProduct={openProductToggle} onLoadProducts={loadProducts} />
|
||||
) },
|
||||
{ key: 'orders', label: '发货订单', disabled: !hasFeature('orders'), children: (
|
||||
<OrdersTab loading={loading} orders={orders} canManage={canManage} onCreateTestOrder={openTestOrderCreate} onLoadOrders={loadOrders} onCopyLink={copyDeliveryLink} onOpenLink={openDeliveryLink} onRevokeLink={revokeDeliveryLink} onRestoreLink={restoreDeliveryLink} />
|
||||
<OrdersTab loading={loading} orders={orders} orderSource={orderSource} canManage={canManage} onCreateManualOrder={openManualOrderCreate} onOrderSourceChange={changeOrderSource} onLoadOrders={loadOrders} onCopyLink={copyDeliveryLink} onOpenLink={openDeliveryLink} onRevokeLink={revokeDeliveryLink} onRestoreLink={restoreDeliveryLink} />
|
||||
) },
|
||||
{ key: 'wallet', label: '钱包', disabled: !hasFeature('wallet'), children: (
|
||||
<WalletTab loading={loading} wallet={wallet} canFinance={canFinance} ledger={ledger} form={walletFilterForm} onFilter={handleWalletFilter} onResetFilter={handleWalletFilterReset} onLoadLedger={handleLoadLedger} />
|
||||
@@ -483,21 +484,27 @@ export default function MerchantCenter({ fixedTab, title = '商户中心' }: Mer
|
||||
/>
|
||||
)}
|
||||
|
||||
<Modal title="创建测试订单" open={testOrderOpen} onOk={submitTestOrder} onCancel={() => setTestOrderOpen(false)} destroyOnClose width={560}>
|
||||
<Form form={testOrderForm} layout="vertical" style={{ marginTop: 16 }}>
|
||||
<Form.Item name="sku" label="商品皮肤" rules={[{ required: true, message: '请选择商品' }]}>
|
||||
<Modal title="创建手动订单" open={manualOrderOpen} confirmLoading={manualOrderSubmitting} onOk={submitManualOrder} onCancel={() => setManualOrderOpen(false)} destroyOnClose width={560}>
|
||||
<Form form={manualOrderForm} layout="vertical" style={{ marginTop: 16 }}>
|
||||
<Form.Item name="client_order_no" label="商户单号" rules={[{ required: true, message: '请输入商户单号' }, { max: 96, message: '商户单号最长 96 个字符' }]}>
|
||||
<Input maxLength={96} />
|
||||
</Form.Item>
|
||||
<Form.Item name="sku" label="商品" rules={[{ required: true, message: '请选择商品' }]}>
|
||||
<Select
|
||||
showSearch
|
||||
loading={testOrderProductsLoading}
|
||||
loading={manualOrderProductsLoading}
|
||||
optionFilterProp="label"
|
||||
options={testOrderProductOptions}
|
||||
options={manualOrderProductOptions}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item name="buyer_reference" label="买家名称" rules={[{ max: 128 }]}>
|
||||
<Input />
|
||||
<Form.Item name="quantity" label="数量" rules={[{ required: true, message: '请输入数量' }]}>
|
||||
<InputNumber min={1} precision={0} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
<Form.Item name="order_status" label="订单状态" rules={[{ required: true }]}>
|
||||
<Select options={testOrderStatusOptions} />
|
||||
<Form.Item name="game_account" label="充值账号 / 游戏 UID" rules={[{ max: 128, message: '账号最长 128 个字符' }]}>
|
||||
<Input maxLength={128} placeholder="用于后续发货时预填" />
|
||||
</Form.Item>
|
||||
<Form.Item name="buyer_reference" label="买家标识" rules={[{ max: 128, message: '买家标识最长 128 个字符' }]}>
|
||||
<Input maxLength={128} />
|
||||
</Form.Item>
|
||||
<Form.Item name="note" label="备注" rules={[{ max: 512 }]}>
|
||||
<Input.TextArea rows={3} />
|
||||
@@ -505,60 +512,6 @@ export default function MerchantCenter({ fixedTab, title = '商户中心' }: Mer
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
title="测试订单"
|
||||
open={!!testOrderResult}
|
||||
onCancel={() => setTestOrderResult(null)}
|
||||
footer={(
|
||||
<Space>
|
||||
<Button onClick={() => setTestOrderResult(null)}>关闭</Button>
|
||||
{testOrderResult?.order.delivery_link_revoked_at ? (
|
||||
<Button onClick={() => restoreDeliveryLink(testOrderResult?.order.order_no || '')}>取消作废</Button>
|
||||
) : (
|
||||
<>
|
||||
<Button
|
||||
icon={<CopyOutlined />}
|
||||
onClick={() => copyDeliveryLink(testOrderResult?.order.order_no || '')}
|
||||
>
|
||||
复制发货链接
|
||||
</Button>
|
||||
<Button type="primary" icon={<LinkOutlined />} onClick={() => openDeliveryLink(testOrderResult?.order.order_no || '')}>
|
||||
打开发货页
|
||||
</Button>
|
||||
<Button danger onClick={() => revokeDeliveryLink(testOrderResult?.order.order_no || '')}>作废链接</Button>
|
||||
</>
|
||||
)}
|
||||
</Space>
|
||||
)}
|
||||
>
|
||||
{testOrderResult && (
|
||||
<Descriptions size="small" bordered column={1} style={{ marginTop: 16 }}>
|
||||
<Descriptions.Item label="订单号">
|
||||
<Typography.Text copyable>{testOrderResult.order.order_no}</Typography.Text>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="SKU">
|
||||
<Typography.Text code>{testOrderResult.order.product_sku}</Typography.Text>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="可发货">
|
||||
<Tag color={testOrderResult.can_ship ? 'green' : 'red'}>
|
||||
{testOrderResult.can_ship ? 'can_ship=true' : 'can_ship=false'}
|
||||
</Tag>
|
||||
</Descriptions.Item>
|
||||
{!testOrderResult.can_ship && (
|
||||
<Descriptions.Item label="不可发货原因">
|
||||
<Typography.Text type="secondary">{testOrderResult.cannot_ship_reason || '-'}</Typography.Text>
|
||||
</Descriptions.Item>
|
||||
)}
|
||||
<Descriptions.Item label="链接有效期">
|
||||
{testOrderResult.order.delivery_link_revoked_at ? <Tag color="red">已作废</Tag> : formatDateTime(testOrderResult.order.delivery_link_expires_at)}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="链接状态">
|
||||
<Typography.Text type="secondary">由后端签名生成,可复制、打开或作废。</Typography.Text>
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
)}
|
||||
</Modal>
|
||||
|
||||
<Modal title="新增 API 密钥" open={apiClientOpen} onOk={submitAPIClient} onCancel={() => setApiClientOpen(false)} destroyOnClose>
|
||||
<Form form={apiClientForm} layout="vertical" style={{ marginTop: 16 }}>
|
||||
<Form.Item
|
||||
|
||||
Reference in New Issue
Block a user