小飞侠接口

This commit is contained in:
2026-07-06 14:12:06 +08:00
parent c87d79109a
commit 80d967f2cf
9 changed files with 652 additions and 6 deletions
+2
View File
@@ -13,6 +13,7 @@ import BenefitCouponsPage from './pages/BenefitCouponsPage';
import BenefitLedgersPage from './pages/BenefitLedgersPage';
import RedeemRecordsPage from './pages/RedeemRecordsPage';
import DeliveriesPage from './pages/DeliveriesPage';
import XiaofeixiaTestPage from './pages/XiaofeixiaTestPage';
import HqAccountsPage from './pages/HqAccountsPage';
import CitiesPage from './pages/CitiesPage';
import StoreMediaPage from './pages/StoreMediaPage';
@@ -60,6 +61,7 @@ export default function App() {
<Route path="/logs/users" element={<UserLogsPage />} />
<Route path="/logs/third-party" element={<ThirdPartyLogsPage />} />
<Route path="/deliveries" element={<DeliveriesPage />} />
<Route path="/deliveries/xiaofeixia" element={<XiaofeixiaTestPage />} />
<Route path="/hq-accounts" element={<HqAccountsPage />} />
</Route>
<Route path="*" element={<Navigate to="/" replace />} />
+10 -2
View File
@@ -67,7 +67,15 @@ const MENU_ITEMS: MenuProps['items'] = [
{ key: '/logs/third-party', label: '第三方日志' },
],
},
{ key: '/deliveries', icon: <CarOutlined />, label: '配送单' },
{
key: 'deliveries-group',
icon: <CarOutlined />,
label: '配送单',
children: [
{ key: '/deliveries', label: '配送单列表' },
{ key: '/deliveries/xiaofeixia', label: '小飞侠联调' },
],
},
{ key: '/hq-accounts', icon: <SafetyOutlined />, label: 'HQ账户' },
];
@@ -95,7 +103,7 @@ export default function AdminLayout() {
theme="dark"
mode="inline"
selectedKeys={[selectedKey]}
defaultOpenKeys={['stores-group', 'partners-group', 'benefit-group', 'logs-group']}
defaultOpenKeys={['stores-group', 'partners-group', 'benefit-group', 'logs-group', 'deliveries-group']}
items={MENU_ITEMS}
onClick={({ key }) => {
if (key.startsWith('/')) navigate(key);
@@ -0,0 +1,290 @@
import { useEffect, useState } from 'react';
import {
Alert, Button, Card, Col, Descriptions, Form, Input, InputNumber, Row, Select, Space,
Tabs, Tag, Typography, message,
} from 'antd';
import { request } from '../lib/api';
type XfxConfig = {
provider: string;
activeProvider: string;
apiUrl: string;
appId: string | null;
mchId: string | null;
mchIdMasked: string | null;
hasApiKey: boolean;
signType: string;
ready: boolean;
};
type ApiResult = {
ok: boolean;
elapsedMs: number;
data?: unknown;
error?: string;
code?: string;
provider?: string;
raw?: unknown;
};
const CREATE_DEFAULTS = {
outNumber: `TEST-${Date.now()}`,
fromName: '杜康仓库',
fromMobile: '13800000000',
fromAddress: '河南省郑州市金水区',
fromAddressDetail: '杜康酒业仓',
fromLng: 113.665,
fromLat: 34.757,
toName: '测试收货人',
toMobile: '13800000001',
toAddress: '河南省郑州市二七区',
toAddressDetail: '测试路 1 号',
toLng: 113.640,
toLat: 34.720,
goodsName: '杜康酒',
goodsNum: 2,
weight: 2,
payMode: '1',
remark: 'Admin 联调测试',
};
function ResultPanel({ result }: { result: ApiResult | null }) {
if (!result) return <Typography.Text type="secondary"></Typography.Text>;
return (
<div>
<Space style={{ marginBottom: 8 }}>
<Tag color={result.ok ? 'green' : 'red'}>{result.ok ? '成功' : '失败'}</Tag>
<span>{result.elapsedMs} ms</span>
{result.code && <span>code: {result.code}</span>}
</Space>
<pre style={{
margin: 0, padding: 12, background: '#f5f5f5', borderRadius: 4,
maxHeight: 360, overflow: 'auto', fontSize: 12,
}}>
{JSON.stringify(result, null, 2)}
</pre>
</div>
);
}
export default function XiaofeixiaTestPage() {
const [config, setConfig] = useState<XfxConfig | null>(null);
const [loading, setLoading] = useState(false);
const [result, setResult] = useState<ApiResult | null>(null);
const [freightForm] = Form.useForm();
const [coverageForm] = Form.useForm();
const [createForm] = Form.useForm();
const [queryForm] = Form.useForm();
const [batchForm] = Form.useForm();
const [trackForm] = Form.useForm();
const [cancelForm] = Form.useForm();
useEffect(() => {
void request<XfxConfig>('/admin/courier/xiaofeixia/config').then(setConfig);
createForm.setFieldsValue(CREATE_DEFAULTS);
freightForm.setFieldsValue({ weight: 2 });
coverageForm.setFieldsValue({ toAddress: '河南省郑州市二七区测试路1号' });
}, [createForm, freightForm, coverageForm]);
async function invoke(path: string, body: unknown) {
setLoading(true);
setResult(null);
try {
const res = await request<ApiResult>(path, {
method: 'POST',
body: JSON.stringify(body),
});
setResult(res);
if (res.ok) message.success('调用成功');
else message.warning(res.error || '调用失败');
} catch (e) {
const err = e instanceof Error ? e.message : String(e);
message.error(err);
setResult({ ok: false, elapsedMs: 0, error: err });
} finally {
setLoading(false);
}
}
function parseList(text?: string) {
return (text ?? '').split(/[,\s]+/).map((s) => s.trim()).filter(Boolean);
}
return (
<div>
<Typography.Title level={4}></Typography.Title>
<Typography.Paragraph type="secondary">
HQ APIcmd 100101~100301
{' '}<code>XIAOFEIXIA_MCH_ID</code><code>XIAOFEIXIA_API_KEY</code>
</Typography.Paragraph>
{config && (
<Card size="small" style={{ marginBottom: 16 }}>
<Descriptions column={3} size="small">
<Descriptions.Item label="Provider">{config.activeProvider}</Descriptions.Item>
<Descriptions.Item label="API">{config.apiUrl}</Descriptions.Item>
<Descriptions.Item label="商户号">{config.mchIdMasked || '—'}</Descriptions.Item>
<Descriptions.Item label="签名">{config.signType}</Descriptions.Item>
<Descriptions.Item label="密钥">
{config.hasApiKey ? <Tag color="green"></Tag> : <Tag color="red"></Tag>}
</Descriptions.Item>
<Descriptions.Item label="状态">
{config.ready ? <Tag color="green"></Tag> : <Tag color="orange"></Tag>}
</Descriptions.Item>
</Descriptions>
{!config.ready && (
<Alert
type="warning"
showIcon
style={{ marginTop: 12 }}
message="请在 server/dukang-api/.env 中配置 XIAOFEIXIA_MCH_ID 与 XIAOFEIXIA_API_KEY 后重启 API"
/>
)}
</Card>
)}
<Row gutter={16}>
<Col xs={24} lg={14}>
<Tabs
items={[
{
key: 'freight',
label: '运费预估 (100105)',
children: (
<Form form={freightForm} layout="vertical" onFinish={(v) => void invoke('/admin/courier/xiaofeixia/estimate-freight', v)}>
<Form.Item name="weight" label="重量 (kg)" rules={[{ required: true }]}>
<InputNumber min={0.01} step={0.5} style={{ width: '100%' }} />
</Form.Item>
<Button type="primary" htmlType="submit" loading={loading}></Button>
</Form>
),
},
{
key: 'coverage',
label: '配送范围 (100301)',
children: (
<Form form={coverageForm} layout="vertical" onFinish={(v) => void invoke('/admin/courier/xiaofeixia/check-coverage', v)}>
<Form.Item name="toAddress" label="收件地址" rules={[{ required: true }]}>
<Input.TextArea rows={2} placeholder="完整收件地址" />
</Form.Item>
<Button type="primary" htmlType="submit" loading={loading}></Button>
</Form>
),
},
{
key: 'create',
label: '创建订单 (100101)',
children: (
<Form form={createForm} layout="vertical" onFinish={(v) => void invoke('/admin/courier/xiaofeixia/create-shipment', v)}>
<Form.Item name="outNumber" label="商家单号 outNumber" rules={[{ required: true }]}>
<Input />
</Form.Item>
<Row gutter={12}>
<Col span={12}>
<Typography.Text strong></Typography.Text>
<Form.Item name="fromName" label="姓名" rules={[{ required: true }]}><Input /></Form.Item>
<Form.Item name="fromMobile" label="手机" rules={[{ required: true }]}><Input /></Form.Item>
<Form.Item name="fromAddress" label="地址" rules={[{ required: true }]}><Input /></Form.Item>
<Form.Item name="fromAddressDetail" label="详细地址" rules={[{ required: true }]}><Input /></Form.Item>
<Space>
<Form.Item name="fromLng" label="经度"><InputNumber style={{ width: 120 }} /></Form.Item>
<Form.Item name="fromLat" label="纬度"><InputNumber style={{ width: 120 }} /></Form.Item>
</Space>
</Col>
<Col span={12}>
<Typography.Text strong></Typography.Text>
<Form.Item name="toName" label="姓名" rules={[{ required: true }]}><Input /></Form.Item>
<Form.Item name="toMobile" label="手机" rules={[{ required: true }]}><Input /></Form.Item>
<Form.Item name="toAddress" label="地址" rules={[{ required: true }]}><Input /></Form.Item>
<Form.Item name="toAddressDetail" label="详细地址" rules={[{ required: true }]}><Input /></Form.Item>
<Space>
<Form.Item name="toLng" label="经度"><InputNumber style={{ width: 120 }} /></Form.Item>
<Form.Item name="toLat" label="纬度"><InputNumber style={{ width: 120 }} /></Form.Item>
</Space>
</Col>
</Row>
<Row gutter={12}>
<Col span={8}><Form.Item name="goodsName" label="货品"><Input /></Form.Item></Col>
<Col span={8}><Form.Item name="goodsNum" label="件数"><InputNumber min={1} style={{ width: '100%' }} /></Form.Item></Col>
<Col span={8}><Form.Item name="weight" label="重量 kg"><InputNumber min={0.01} style={{ width: '100%' }} /></Form.Item></Col>
</Row>
<Form.Item name="payMode" label="付费方式" rules={[{ required: true }]}>
<Select options={[
{ value: '1', label: '寄付' },
{ value: '2', label: '到付' },
]} />
</Form.Item>
<Form.Item name="remark" label="备注"><Input /></Form.Item>
<Space>
<Button type="primary" htmlType="submit" loading={loading}></Button>
<Button onClick={() => createForm.setFieldsValue({ ...CREATE_DEFAULTS, outNumber: `TEST-${Date.now()}` })}>
</Button>
</Space>
</Form>
),
},
{
key: 'query',
label: '查询订单 (100104)',
children: (
<Form form={queryForm} layout="vertical" onFinish={(v) => void invoke('/admin/courier/xiaofeixia/get-shipment', v)}>
<Form.Item name="trackingNumber" label="运单号 number"><Input placeholder="小飞侠运单号" /></Form.Item>
<Form.Item name="outNumber" label="商家单号 outNumber"><Input /></Form.Item>
<Button type="primary" htmlType="submit" loading={loading}></Button>
</Form>
),
},
{
key: 'batch',
label: '批量查询 (100106)',
children: (
<Form form={batchForm} layout="vertical" onFinish={(v) => void invoke('/admin/courier/xiaofeixia/batch-get-shipments', {
trackingNumbers: parseList(v.trackingNumbersText),
outNumbers: parseList(v.outNumbersText),
})}>
<Form.Item name="trackingNumbersText" label="运单号(逗号分隔)">
<Input.TextArea rows={2} placeholder="NO001,NO002" />
</Form.Item>
<Form.Item name="outNumbersText" label="商家单号(逗号分隔)">
<Input.TextArea rows={2} />
</Form.Item>
<Button type="primary" htmlType="submit" loading={loading}></Button>
</Form>
),
},
{
key: 'track',
label: '路由查询 (100102)',
children: (
<Form form={trackForm} layout="vertical" onFinish={(v) => void invoke('/admin/courier/xiaofeixia/get-track', v)}>
<Form.Item name="trackingNumber" label="运单号"><Input /></Form.Item>
<Form.Item name="outNumber" label="商家单号"><Input /></Form.Item>
<Button type="primary" htmlType="submit" loading={loading}></Button>
</Form>
),
},
{
key: 'cancel',
label: '取消订单 (100103)',
children: (
<Form form={cancelForm} layout="vertical" onFinish={(v) => void invoke('/admin/courier/xiaofeixia/cancel-shipment', v)}>
<Form.Item name="trackingNumber" label="运单号"><Input /></Form.Item>
<Form.Item name="outNumber" label="商家单号"><Input /></Form.Item>
<Button type="primary" danger htmlType="submit" loading={loading}></Button>
</Form>
),
},
]}
/>
</Col>
<Col xs={24} lg={10}>
<Card title="响应结果" size="small">
<ResultPanel result={result} />
</Card>
</Col>
</Row>
</div>
);
}