feat: 实现多商户管理模块与前端服务

refactor: 优化服务层代码并修复类型问题

docs: 更新开发进度文档

feat(merchant): 新增商户监控与数据统计服务

feat(dashboard): 添加商户管理前端页面与服务

fix: 修复类型转换与可选参数处理

feat: 实现商户订单、店铺与结算管理功能

refactor: 重构审计日志格式与服务调用

feat: 新增商户入驻与身份注册功能

fix(controller): 修复路由参数类型问题

feat: 添加商户排名与统计报告功能

chore: 更新模拟数据与服务配置
This commit is contained in:
2026-03-18 13:38:05 +08:00
parent 86ec0fe253
commit b31591e04c
57 changed files with 24055 additions and 157 deletions

File diff suppressed because one or more lines are too long

View File

@@ -19,25 +19,31 @@ if (process.env.NODE_ENV === 'development') {
import React from 'react';
export async function getRoutes() {
const routes = {"Compliance/CertificateExpiryReminder":{"path":"Compliance/CertificateExpiryReminder","id":"Compliance/CertificateExpiryReminder"},"Compliance/CertificateManage":{"path":"Compliance/CertificateManage","id":"Compliance/CertificateManage"},"AfterSales/CustomerService":{"path":"AfterSales/CustomerService","id":"AfterSales/CustomerService"},"Compliance/ComplianceCheck":{"path":"Compliance/ComplianceCheck","id":"Compliance/ComplianceCheck"},"Product/ProductPublishForm":{"path":"Product/ProductPublishForm","id":"Product/ProductPublishForm"},"Logistics/LogisticsSelect":{"path":"Logistics/LogisticsSelect","id":"Logistics/LogisticsSelect"},"AfterSales/RefundProcess":{"path":"AfterSales/RefundProcess","id":"AfterSales/RefundProcess"},"B2BTrade/EnterpriseQuote":{"path":"B2BTrade/EnterpriseQuote","id":"B2BTrade/EnterpriseQuote"},"Logistics/LogisticsTrack":{"path":"Logistics/LogisticsTrack","id":"Logistics/LogisticsTrack"},"B2BTrade/ContractManage":{"path":"B2BTrade/ContractManage","id":"B2BTrade/ContractManage"},"Orders/OrderAggregation":{"path":"Orders/OrderAggregation","id":"Orders/OrderAggregation"},"AfterSales/ReturnApply":{"path":"AfterSales/ReturnApply","id":"AfterSales/ReturnApply"},"Auth/ResetPasswordPage":{"path":"Auth/ResetPasswordPage","id":"Auth/ResetPasswordPage"},"Product/MaterialUpload":{"path":"Product/MaterialUpload","id":"Product/MaterialUpload"},"UserAsset/PointsManage":{"path":"UserAsset/PointsManage","id":"UserAsset/PointsManage"},"Logistics/FreightCalc":{"path":"Logistics/FreightCalc","id":"Logistics/FreightCalc"},"Orders/ExceptionOrder":{"path":"Orders/ExceptionOrder","id":"Orders/ExceptionOrder"},"Product/ProductDetail":{"path":"Product/ProductDetail","id":"Product/ProductDetail"},"UserAsset/MemberLevel":{"path":"UserAsset/MemberLevel","id":"UserAsset/MemberLevel"},"ABTest/ABTestResults":{"path":"ABTest/ABTestResults","id":"ABTest/ABTestResults"},"UserAsset/UserAssets":{"path":"UserAsset/UserAssets","id":"UserAsset/UserAssets"},"ABTest/ABTestConfig":{"path":"ABTest/ABTestConfig","id":"ABTest/ABTestConfig"},"B2B/EnterpriseQuote":{"path":"B2B/EnterpriseQuote","id":"B2B/EnterpriseQuote"},"B2BTrade/BatchOrder":{"path":"B2BTrade/BatchOrder","id":"B2BTrade/BatchOrder"},"B2B/ContractManage":{"path":"B2B/ContractManage","id":"B2B/ContractManage"},"Orders/OrderDetail":{"path":"Orders/OrderDetail","id":"Orders/OrderDetail"},"Auth/RegisterPage":{"path":"Auth/RegisterPage","id":"Auth/RegisterPage"},"AfterSales/index":{"path":"AfterSales","id":"AfterSales/index"},"Compliance/index":{"path":"Compliance","id":"Compliance/index"},"Orders/OrderList":{"path":"Orders/OrderList","id":"Orders/OrderList"},"Logistics/index":{"path":"Logistics","id":"Logistics/index"},"UserAsset/index":{"path":"UserAsset","id":"UserAsset/index"},"Ad/ROIAnalysis":{"path":"Ad/ROIAnalysis","id":"Ad/ROIAnalysis"},"Auth/LoginPage":{"path":"Auth/LoginPage","id":"Auth/LoginPage"},"B2B/BatchOrder":{"path":"B2B/BatchOrder","id":"B2B/BatchOrder"},"B2BTrade/index":{"path":"B2BTrade","id":"B2BTrade/index"},"Ad/AdDelivery":{"path":"Ad/AdDelivery","id":"Ad/AdDelivery"},"Ad/AdPlanPage":{"path":"Ad/AdPlanPage","id":"Ad/AdPlanPage"},"Product/index":{"path":"Product","id":"Product/index"},"ABTest/index":{"path":"ABTest","id":"ABTest/index"},"Orders/index":{"path":"Orders","id":"Orders/index"},"Auth/index":{"path":"Auth","id":"Auth/index"},"B2B/index":{"path":"B2B","id":"B2B/index"},"Ad/index":{"path":"Ad","id":"Ad/index"}} as const;
const routes = {"Compliance/CertificateExpiryReminder":{"path":"Compliance/CertificateExpiryReminder","id":"Compliance/CertificateExpiryReminder"},"Merchant/MerchantSettlementManage":{"path":"Merchant/MerchantSettlementManage","id":"Merchant/MerchantSettlementManage"},"Compliance/CertificateManage":{"path":"Compliance/CertificateManage","id":"Compliance/CertificateManage"},"Merchant/MerchantOrderManage":{"path":"Merchant/MerchantOrderManage","id":"Merchant/MerchantOrderManage"},"Merchant/MerchantShopManage":{"path":"Merchant/MerchantShopManage","id":"Merchant/MerchantShopManage"},"AfterSales/CustomerService":{"path":"AfterSales/CustomerService","id":"AfterSales/CustomerService"},"Compliance/ComplianceCheck":{"path":"Compliance/ComplianceCheck","id":"Compliance/ComplianceCheck"},"Product/ProductPublishForm":{"path":"Product/ProductPublishForm","id":"Product/ProductPublishForm"},"Blacklist/BlacklistManage":{"path":"Blacklist/BlacklistManage","id":"Blacklist/BlacklistManage"},"Logistics/LogisticsSelect":{"path":"Logistics/LogisticsSelect","id":"Logistics/LogisticsSelect"},"AfterSales/RefundProcess":{"path":"AfterSales/RefundProcess","id":"AfterSales/RefundProcess"},"B2BTrade/EnterpriseQuote":{"path":"B2BTrade/EnterpriseQuote","id":"B2BTrade/EnterpriseQuote"},"Logistics/LogisticsTrack":{"path":"Logistics/LogisticsTrack","id":"Logistics/LogisticsTrack"},"B2BTrade/ContractManage":{"path":"B2BTrade/ContractManage","id":"B2BTrade/ContractManage"},"Merchant/MerchantManage":{"path":"Merchant/MerchantManage","id":"Merchant/MerchantManage"},"Orders/OrderAggregation":{"path":"Orders/OrderAggregation","id":"Orders/OrderAggregation"},"AfterSales/ReturnApply":{"path":"AfterSales/ReturnApply","id":"AfterSales/ReturnApply"},"Auth/ResetPasswordPage":{"path":"Auth/ResetPasswordPage","id":"Auth/ResetPasswordPage"},"Product/MaterialUpload":{"path":"Product/MaterialUpload","id":"Product/MaterialUpload"},"UserAsset/PointsManage":{"path":"UserAsset/PointsManage","id":"UserAsset/PointsManage"},"Blacklist/RiskMonitor":{"path":"Blacklist/RiskMonitor","id":"Blacklist/RiskMonitor"},"Logistics/FreightCalc":{"path":"Logistics/FreightCalc","id":"Logistics/FreightCalc"},"Orders/ExceptionOrder":{"path":"Orders/ExceptionOrder","id":"Orders/ExceptionOrder"},"Product/ProductDetail":{"path":"Product/ProductDetail","id":"Product/ProductDetail"},"UserAsset/MemberLevel":{"path":"UserAsset/MemberLevel","id":"UserAsset/MemberLevel"},"ABTest/ABTestResults":{"path":"ABTest/ABTestResults","id":"ABTest/ABTestResults"},"UserAsset/UserAssets":{"path":"UserAsset/UserAssets","id":"UserAsset/UserAssets"},"ABTest/ABTestConfig":{"path":"ABTest/ABTestConfig","id":"ABTest/ABTestConfig"},"B2B/EnterpriseQuote":{"path":"B2B/EnterpriseQuote","id":"B2B/EnterpriseQuote"},"B2BTrade/BatchOrder":{"path":"B2BTrade/BatchOrder","id":"B2BTrade/BatchOrder"},"B2B/ContractManage":{"path":"B2B/ContractManage","id":"B2B/ContractManage"},"Orders/OrderDetail":{"path":"Orders/OrderDetail","id":"Orders/OrderDetail"},"Auth/RegisterPage":{"path":"Auth/RegisterPage","id":"Auth/RegisterPage"},"AfterSales/index":{"path":"AfterSales","id":"AfterSales/index"},"Compliance/index":{"path":"Compliance","id":"Compliance/index"},"Orders/OrderList":{"path":"Orders/OrderList","id":"Orders/OrderList"},"Logistics/index":{"path":"Logistics","id":"Logistics/index"},"UserAsset/index":{"path":"UserAsset","id":"UserAsset/index"},"Ad/ROIAnalysis":{"path":"Ad/ROIAnalysis","id":"Ad/ROIAnalysis"},"Auth/LoginPage":{"path":"Auth/LoginPage","id":"Auth/LoginPage"},"B2B/BatchOrder":{"path":"B2B/BatchOrder","id":"B2B/BatchOrder"},"B2BTrade/index":{"path":"B2BTrade","id":"B2BTrade/index"},"Merchant/index":{"path":"Merchant","id":"Merchant/index"},"Ad/AdDelivery":{"path":"Ad/AdDelivery","id":"Ad/AdDelivery"},"Ad/AdPlanPage":{"path":"Ad/AdPlanPage","id":"Ad/AdPlanPage"},"Product/index":{"path":"Product","id":"Product/index"},"ABTest/index":{"path":"ABTest","id":"ABTest/index"},"Orders/index":{"path":"Orders","id":"Orders/index"},"Auth/index":{"path":"Auth","id":"Auth/index"},"B2B/index":{"path":"B2B","id":"B2B/index"},"Ad/index":{"path":"Ad","id":"Ad/index"}} as const;
return {
routes,
routeComponents: {
'Compliance/CertificateExpiryReminder': React.lazy(() => import(/* webpackChunkName: "src__pages__Compliance__CertificateExpiryReminder" */'../../../src/pages/Compliance/CertificateExpiryReminder.tsx')),
'Merchant/MerchantSettlementManage': React.lazy(() => import(/* webpackChunkName: "src__pages__Merchant__MerchantSettlementManage" */'../../../src/pages/Merchant/MerchantSettlementManage.tsx')),
'Compliance/CertificateManage': React.lazy(() => import(/* webpackChunkName: "src__pages__Compliance__CertificateManage" */'../../../src/pages/Compliance/CertificateManage.tsx')),
'Merchant/MerchantOrderManage': React.lazy(() => import(/* webpackChunkName: "src__pages__Merchant__MerchantOrderManage" */'../../../src/pages/Merchant/MerchantOrderManage.tsx')),
'Merchant/MerchantShopManage': React.lazy(() => import(/* webpackChunkName: "src__pages__Merchant__MerchantShopManage" */'../../../src/pages/Merchant/MerchantShopManage.tsx')),
'AfterSales/CustomerService': React.lazy(() => import(/* webpackChunkName: "src__pages__AfterSales__CustomerService" */'../../../src/pages/AfterSales/CustomerService.tsx')),
'Compliance/ComplianceCheck': React.lazy(() => import(/* webpackChunkName: "src__pages__Compliance__ComplianceCheck" */'../../../src/pages/Compliance/ComplianceCheck.tsx')),
'Product/ProductPublishForm': React.lazy(() => import(/* webpackChunkName: "src__pages__Product__ProductPublishForm" */'../../../src/pages/Product/ProductPublishForm.tsx')),
'Blacklist/BlacklistManage': React.lazy(() => import(/* webpackChunkName: "src__pages__Blacklist__BlacklistManage" */'../../../src/pages/Blacklist/BlacklistManage.tsx')),
'Logistics/LogisticsSelect': React.lazy(() => import(/* webpackChunkName: "src__pages__Logistics__LogisticsSelect" */'../../../src/pages/Logistics/LogisticsSelect.tsx')),
'AfterSales/RefundProcess': React.lazy(() => import(/* webpackChunkName: "src__pages__AfterSales__RefundProcess" */'../../../src/pages/AfterSales/RefundProcess.tsx')),
'B2BTrade/EnterpriseQuote': React.lazy(() => import(/* webpackChunkName: "src__pages__B2BTrade__EnterpriseQuote" */'../../../src/pages/B2BTrade/EnterpriseQuote.tsx')),
'Logistics/LogisticsTrack': React.lazy(() => import(/* webpackChunkName: "src__pages__Logistics__LogisticsTrack" */'../../../src/pages/Logistics/LogisticsTrack.tsx')),
'B2BTrade/ContractManage': React.lazy(() => import(/* webpackChunkName: "src__pages__B2BTrade__ContractManage" */'../../../src/pages/B2BTrade/ContractManage.tsx')),
'Merchant/MerchantManage': React.lazy(() => import(/* webpackChunkName: "src__pages__Merchant__MerchantManage" */'../../../src/pages/Merchant/MerchantManage.tsx')),
'Orders/OrderAggregation': React.lazy(() => import(/* webpackChunkName: "src__pages__Orders__OrderAggregation" */'../../../src/pages/Orders/OrderAggregation.tsx')),
'AfterSales/ReturnApply': React.lazy(() => import(/* webpackChunkName: "src__pages__AfterSales__ReturnApply" */'../../../src/pages/AfterSales/ReturnApply.tsx')),
'Auth/ResetPasswordPage': React.lazy(() => import(/* webpackChunkName: "src__pages__Auth__ResetPasswordPage" */'../../../src/pages/Auth/ResetPasswordPage.tsx')),
'Product/MaterialUpload': React.lazy(() => import(/* webpackChunkName: "src__pages__Product__MaterialUpload" */'../../../src/pages/Product/MaterialUpload.tsx')),
'UserAsset/PointsManage': React.lazy(() => import(/* webpackChunkName: "src__pages__UserAsset__PointsManage" */'../../../src/pages/UserAsset/PointsManage.tsx')),
'Blacklist/RiskMonitor': React.lazy(() => import(/* webpackChunkName: "src__pages__Blacklist__RiskMonitor" */'../../../src/pages/Blacklist/RiskMonitor.tsx')),
'Logistics/FreightCalc': React.lazy(() => import(/* webpackChunkName: "src__pages__Logistics__FreightCalc" */'../../../src/pages/Logistics/FreightCalc.tsx')),
'Orders/ExceptionOrder': React.lazy(() => import(/* webpackChunkName: "src__pages__Orders__ExceptionOrder" */'../../../src/pages/Orders/ExceptionOrder.tsx')),
'Product/ProductDetail': React.lazy(() => import(/* webpackChunkName: "src__pages__Product__ProductDetail" */'../../../src/pages/Product/ProductDetail.tsx')),
@@ -59,6 +65,7 @@ export async function getRoutes() {
'Auth/LoginPage': React.lazy(() => import(/* webpackChunkName: "src__pages__Auth__LoginPage" */'../../../src/pages/Auth/LoginPage.tsx')),
'B2B/BatchOrder': React.lazy(() => import(/* webpackChunkName: "src__pages__B2B__BatchOrder" */'../../../src/pages/B2B/BatchOrder.tsx')),
'B2BTrade/index': React.lazy(() => import(/* webpackChunkName: "src__pages__B2BTrade__index" */'../../../src/pages/B2BTrade/index.ts')),
'Merchant/index': React.lazy(() => import(/* webpackChunkName: "src__pages__Merchant__index" */'../../../src/pages/Merchant/index.ts')),
'Ad/AdDelivery': React.lazy(() => import(/* webpackChunkName: "src__pages__Ad__AdDelivery" */'../../../src/pages/Ad/AdDelivery.tsx')),
'Ad/AdPlanPage': React.lazy(() => import(/* webpackChunkName: "src__pages__Ad__AdPlanPage" */'../../../src/pages/Ad/AdPlanPage.tsx')),
'Product/index': React.lazy(() => import(/* webpackChunkName: "src__pages__Product__index" */'../../../src/pages/Product/index.ts')),

View File

@@ -0,0 +1,829 @@
import React, { useState, useEffect } from 'react';
import {
Card,
Table,
Button,
Space,
Modal,
Form,
Input,
Select,
DatePicker,
InputNumber,
Descriptions,
Divider,
message,
Tag,
Tabs,
Row,
Col,
Statistic,
Alert,
Tooltip,
Badge,
Upload,
} from 'antd';
import {
UserOutlined,
PlusOutlined,
EyeOutlined,
EditOutlined,
DeleteOutlined,
SearchOutlined,
WarningOutlined,
CheckCircleOutlined,
CloseCircleOutlined,
StopOutlined,
ExclamationCircleOutlined,
UploadOutlined,
DownloadOutlined,
} from '@ant-design/icons';
import type { ColumnsType } from 'antd/es/table';
import dayjs from 'dayjs';
const { Option } = Select;
const { TabPane } = Tabs;
const { TextArea } = Input;
interface BlacklistRecord {
id: string;
tenant_id: string;
shop_id: string;
buyer_id: string;
buyer_name: string;
buyer_email: string;
buyer_phone?: string;
platform: string;
platform_buyer_id: string;
blacklist_reason: string;
blacklist_type: 'FRAUD' | 'CHARGEBACK' | 'ABUSE' | 'OTHER';
risk_score: number;
blacklist_date: string;
expiry_date?: string;
status: 'ACTIVE' | 'INACTIVE' | 'EXPIRED';
evidence?: string;
created_by: string;
trace_id: string;
created_at: string;
updated_at: string;
}
const BLACKLIST_TYPES = [
{ value: 'FRAUD', label: 'Fraud', color: 'red' },
{ value: 'CHARGEBACK', label: 'Chargeback', color: 'orange' },
{ value: 'ABUSE', label: 'Abuse', color: 'purple' },
{ value: 'OTHER', label: 'Other', color: 'default' },
];
const STATUS_CONFIG: Record<string, { color: string; text: string; icon: React.ReactNode }> = {
ACTIVE: { color: 'error', text: 'Active', icon: <StopOutlined /> },
INACTIVE: { color: 'default', text: 'Inactive', icon: <CloseCircleOutlined /> },
EXPIRED: { color: 'default', text: 'Expired', icon: <ExclamationCircleOutlined /> },
};
const PLATFORMS = [
'Amazon',
'eBay',
'Shopify',
'Walmart',
'TikTok Shop',
'Temu',
'Alibaba',
'Other',
];
const MOCK_BLACKLISTS: BlacklistRecord[] = [
{
id: '1',
tenant_id: 'tenant-001',
shop_id: 'shop-001',
buyer_id: 'buyer-001',
buyer_name: 'John Smith',
buyer_email: 'john.smith@example.com',
buyer_phone: '+1-555-0101',
platform: 'Amazon',
platform_buyer_id: 'AMZ-001',
blacklist_reason: 'Multiple fraudulent transactions',
blacklist_type: 'FRAUD',
risk_score: 85,
blacklist_date: '2026-01-15',
expiry_date: '2027-01-15',
status: 'ACTIVE',
evidence: 'Transaction records, IP logs',
created_by: 'admin',
trace_id: 'trace-001',
created_at: '2026-01-15 10:00:00',
updated_at: '2026-01-15 10:00:00',
},
{
id: '2',
tenant_id: 'tenant-001',
shop_id: 'shop-001',
buyer_id: 'buyer-002',
buyer_name: 'Jane Doe',
buyer_email: 'jane.doe@example.com',
platform: 'eBay',
platform_buyer_id: 'EBAY-002',
blacklist_reason: 'Excessive chargebacks',
blacklist_type: 'CHARGEBACK',
risk_score: 75,
blacklist_date: '2026-02-20',
expiry_date: '2026-08-20',
status: 'ACTIVE',
evidence: 'Chargeback documentation',
created_by: 'admin',
trace_id: 'trace-002',
created_at: '2026-02-20 14:30:00',
updated_at: '2026-02-20 14:30:00',
},
{
id: '3',
tenant_id: 'tenant-001',
shop_id: 'shop-001',
buyer_id: 'buyer-003',
buyer_name: 'Mike Johnson',
buyer_email: 'mike.j@example.com',
platform: 'Amazon',
platform_buyer_id: 'AMZ-003',
blacklist_reason: 'Abusive behavior towards support',
blacklist_type: 'ABUSE',
risk_score: 60,
blacklist_date: '2026-03-01',
expiry_date: '2026-06-01',
status: 'ACTIVE',
evidence: 'Support chat logs',
created_by: 'admin',
trace_id: 'trace-003',
created_at: '2026-03-01 09:15:00',
updated_at: '2026-03-01 09:15:00',
},
{
id: '4',
tenant_id: 'tenant-001',
shop_id: 'shop-001',
buyer_id: 'buyer-004',
buyer_name: 'Sarah Wilson',
buyer_email: 'sarah.w@example.com',
platform: 'Shopify',
platform_buyer_id: 'SHOPIFY-004',
blacklist_reason: 'Suspicious ordering pattern',
blacklist_type: 'OTHER',
risk_score: 50,
blacklist_date: '2026-02-10',
expiry_date: '2026-05-10',
status: 'INACTIVE',
evidence: 'Order history analysis',
created_by: 'admin',
trace_id: 'trace-004',
created_at: '2026-02-10 11:00:00',
updated_at: '2026-03-01 16:00:00',
},
];
const BlacklistManage: React.FC = () => {
const [form] = Form.useForm();
const [loading, setLoading] = useState(false);
const [blacklists, setBlacklists] = useState<BlacklistRecord[]>(MOCK_BLACKLISTS);
const [createModalVisible, setCreateModalVisible] = useState(false);
const [editModalVisible, setEditModalVisible] = useState(false);
const [detailModalVisible, setDetailModalVisible] = useState(false);
const [selectedBlacklist, setSelectedBlacklist] = useState<BlacklistRecord | null>(null);
const [activeTab, setActiveTab] = useState('all');
const [searchText, setSearchText] = useState('');
const [platformFilter, setPlatformFilter] = useState<string>('');
const [typeFilter, setTypeFilter] = useState<string>('');
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
useEffect(() => {
fetchBlacklists();
}, []);
const fetchBlacklists = async () => {
setLoading(true);
try {
setBlacklists(MOCK_BLACKLISTS);
} finally {
setLoading(false);
}
};
const handleCreate = async (values: any) => {
setLoading(true);
try {
const newBlacklist: BlacklistRecord = {
id: `${Date.now()}`,
tenant_id: 'tenant-001',
shop_id: 'shop-001',
buyer_id: `buyer-${Date.now()}`,
buyer_name: values.buyer_name,
buyer_email: values.buyer_email,
buyer_phone: values.buyer_phone,
platform: values.platform,
platform_buyer_id: values.platform_buyer_id,
blacklist_reason: values.blacklist_reason,
blacklist_type: values.blacklist_type,
risk_score: values.risk_score,
blacklist_date: dayjs().format('YYYY-MM-DD'),
expiry_date: values.expiry_date ? values.expiry_date.format('YYYY-MM-DD') : undefined,
status: 'ACTIVE',
evidence: values.evidence,
created_by: 'admin',
trace_id: `trace-${Date.now()}`,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
};
setBlacklists([newBlacklist, ...blacklists]);
message.success('Buyer added to blacklist successfully');
setCreateModalVisible(false);
form.resetFields();
} finally {
setLoading(false);
}
};
const handleEdit = async (values: any) => {
if (!selectedBlacklist) return;
setLoading(true);
try {
const updatedBlacklists = blacklists.map(bl =>
bl.id === selectedBlacklist.id
? {
...bl,
buyer_name: values.buyer_name,
buyer_email: values.buyer_email,
buyer_phone: values.buyer_phone,
blacklist_reason: values.blacklist_reason,
blacklist_type: values.blacklist_type,
risk_score: values.risk_score,
expiry_date: values.expiry_date ? values.expiry_date.format('YYYY-MM-DD') : undefined,
status: values.status,
evidence: values.evidence,
updated_at: new Date().toISOString(),
}
: bl
);
setBlacklists(updatedBlacklists);
message.success('Blacklist record updated successfully');
setEditModalVisible(false);
form.resetFields();
setSelectedBlacklist(null);
} finally {
setLoading(false);
}
};
const handleDelete = (id: string) => {
Modal.confirm({
title: 'Remove from Blacklist',
content: 'Are you sure you want to remove this buyer from the blacklist?',
okType: 'danger',
onOk: () => {
const updatedBlacklists = blacklists.map(bl =>
bl.id === id ? { ...bl, status: 'INACTIVE' as const, updated_at: new Date().toISOString() } : bl
);
setBlacklists(updatedBlacklists);
message.success('Buyer removed from blacklist');
},
});
};
const handleBatchDelete = () => {
if (selectedRowKeys.length === 0) {
message.warning('Please select records to delete');
return;
}
Modal.confirm({
title: 'Batch Remove from Blacklist',
content: `Are you sure you want to remove ${selectedRowKeys.length} buyers from the blacklist?`,
okType: 'danger',
onOk: () => {
const updatedBlacklists = blacklists.map(bl =>
selectedRowKeys.includes(bl.id) ? { ...bl, status: 'INACTIVE' as const, updated_at: new Date().toISOString() } : bl
);
setBlacklists(updatedBlacklists);
setSelectedRowKeys([]);
message.success(`${selectedRowKeys.length} buyers removed from blacklist`);
},
});
};
const handleViewDetail = (blacklist: BlacklistRecord) => {
setSelectedBlacklist(blacklist);
setDetailModalVisible(true);
};
const handleEditClick = (blacklist: BlacklistRecord) => {
setSelectedBlacklist(blacklist);
form.setFieldsValue({
buyer_name: blacklist.buyer_name,
buyer_email: blacklist.buyer_email,
buyer_phone: blacklist.buyer_phone,
platform: blacklist.platform,
platform_buyer_id: blacklist.platform_buyer_id,
blacklist_reason: blacklist.blacklist_reason,
blacklist_type: blacklist.blacklist_type,
risk_score: blacklist.risk_score,
expiry_date: blacklist.expiry_date ? dayjs(blacklist.expiry_date) : null,
status: blacklist.status,
evidence: blacklist.evidence,
});
setEditModalVisible(true);
};
const getFilteredBlacklists = () => {
let filtered = blacklists;
if (activeTab !== 'all') {
filtered = filtered.filter(bl => bl.status === activeTab.toUpperCase());
}
if (searchText) {
filtered = filtered.filter(bl =>
bl.buyer_name.toLowerCase().includes(searchText.toLowerCase()) ||
bl.buyer_email.toLowerCase().includes(searchText.toLowerCase()) ||
bl.platform_buyer_id.toLowerCase().includes(searchText.toLowerCase())
);
}
if (platformFilter) {
filtered = filtered.filter(bl => bl.platform === platformFilter);
}
if (typeFilter) {
filtered = filtered.filter(bl => bl.blacklist_type === typeFilter);
}
return filtered;
};
const getRiskLevel = (score: number): { level: string; color: string } => {
if (score >= 80) return { level: 'Critical', color: '#ff4d4f' };
if (score >= 60) return { level: 'High', color: '#ff7a45' };
if (score >= 40) return { level: 'Medium', color: '#faad14' };
return { level: 'Low', color: '#52c41a' };
};
const columns: ColumnsType<BlacklistRecord> = [
{
title: 'Buyer Name',
dataIndex: 'buyer_name',
key: 'buyer_name',
width: 150,
render: (name: string, record: BlacklistRecord) => (
<a onClick={() => handleViewDetail(record)}>{name}</a>
),
},
{
title: 'Email',
dataIndex: 'buyer_email',
key: 'buyer_email',
width: 200,
ellipsis: true,
},
{
title: 'Platform',
dataIndex: 'platform',
key: 'platform',
width: 100,
},
{
title: 'Platform Buyer ID',
dataIndex: 'platform_buyer_id',
key: 'platform_buyer_id',
width: 150,
},
{
title: 'Type',
dataIndex: 'blacklist_type',
key: 'blacklist_type',
width: 100,
render: (type: string) => {
const config = BLACKLIST_TYPES.find(t => t.value === type);
return <Tag color={config?.color}>{type}</Tag>;
},
},
{
title: 'Risk Score',
dataIndex: 'risk_score',
key: 'risk_score',
width: 120,
render: (score: number) => {
const { level, color } = getRiskLevel(score);
return (
<Tag color={color}>
{score} - {level}
</Tag>
);
},
sorter: (a, b) => a.risk_score - b.risk_score,
},
{
title: 'Blacklist Date',
dataIndex: 'blacklist_date',
key: 'blacklist_date',
width: 120,
},
{
title: 'Expiry Date',
dataIndex: 'expiry_date',
key: 'expiry_date',
width: 120,
render: (date: string) => {
if (!date) return '-';
const daysUntilExpiry = dayjs(date).diff(dayjs(), 'day');
const isExpiringSoon = daysUntilExpiry > 0 && daysUntilExpiry <= 30;
return (
<Tooltip title={isExpiringSoon ? `Expires in ${daysUntilExpiry} days` : ''}>
<span style={{ color: isExpiringSoon || daysUntilExpiry <= 0 ? '#ff4d4f' : undefined }}>
{date}
</span>
</Tooltip>
);
},
},
{
title: 'Status',
dataIndex: 'status',
key: 'status',
width: 100,
render: (status: string) => {
const config = STATUS_CONFIG[status];
return <Tag color={config.color} icon={config.icon}>{config.text}</Tag>;
},
},
{
title: 'Actions',
key: 'actions',
width: 150,
render: (_, record: BlacklistRecord) => (
<Space>
<Tooltip title="View Details">
<Button type="link" icon={<EyeOutlined />} onClick={() => handleViewDetail(record)} />
</Tooltip>
<Tooltip title="Edit">
<Button type="link" icon={<EditOutlined />} onClick={() => handleEditClick(record)} />
</Tooltip>
{record.status === 'ACTIVE' && (
<Tooltip title="Remove">
<Button type="link" danger icon={<DeleteOutlined />} onClick={() => handleDelete(record.id)} />
</Tooltip>
)}
</Space>
),
},
];
const stats = {
total: blacklists.length,
active: blacklists.filter(bl => bl.status === 'ACTIVE').length,
inactive: blacklists.filter(bl => bl.status === 'INACTIVE').length,
expired: blacklists.filter(bl => bl.status === 'EXPIRED').length,
highRisk: blacklists.filter(bl => bl.risk_score >= 60).length,
};
return (
<div className="blacklist-manage-page">
<Row gutter={16} style={{ marginBottom: 24 }}>
<Col span={4}>
<Card>
<Statistic title="Total Blacklisted" value={stats.total} prefix={<UserOutlined />} />
</Card>
</Col>
<Col span={4}>
<Card>
<Statistic title="Active" value={stats.active} valueStyle={{ color: '#ff4d4f' }} prefix={<StopOutlined />} />
</Card>
</Col>
<Col span={4}>
<Card>
<Statistic title="Inactive" value={stats.inactive} valueStyle={{ color: '#8c8c8c' }} prefix={<CloseCircleOutlined />} />
</Card>
</Col>
<Col span={4}>
<Card>
<Statistic title="High Risk" value={stats.highRisk} valueStyle={{ color: '#ff7a45' }} prefix={<WarningOutlined />} />
</Card>
</Col>
<Col span={8}>
<Card>
<Alert
message="Blacklist Alert"
description={`${stats.active} buyers are currently blacklisted. ${stats.highRisk} are high risk.`}
type="error"
showIcon
icon={<WarningOutlined />}
/>
</Card>
</Col>
</Row>
<Card title="Blacklist Management" extra={
<Space>
{selectedRowKeys.length > 0 && (
<Button danger icon={<DeleteOutlined />} onClick={handleBatchDelete}>
Batch Remove ({selectedRowKeys.length})
</Button>
)}
<Button type="primary" icon={<PlusOutlined />} onClick={() => setCreateModalVisible(true)}>
Add to Blacklist
</Button>
</Space>
}>
<div style={{ display: 'flex', marginBottom: 16, gap: 16 }}>
<Input
placeholder="Search by name, email, or platform buyer ID"
prefix={<SearchOutlined />}
style={{ width: 300 }}
onChange={(e) => setSearchText(e.target.value)}
/>
<Select
placeholder="Filter by Platform"
style={{ width: 150 }}
onChange={setPlatformFilter}
allowClear
>
{PLATFORMS.map(platform => (
<Option key={platform} value={platform}>{platform}</Option>
))}
</Select>
<Select
placeholder="Filter by Type"
style={{ width: 150 }}
onChange={setTypeFilter}
allowClear
>
{BLACKLIST_TYPES.map(type => (
<Option key={type.value} value={type.value}>{type.label}</Option>
))}
</Select>
</div>
<Tabs activeKey={activeTab} onChange={setActiveTab}>
<TabPane tab="All" key="all" />
<TabPane tab="Active" key="active" />
<TabPane tab="Inactive" key="inactive" />
<TabPane tab="Expired" key="expired" />
</Tabs>
<Table
columns={columns}
dataSource={getFilteredBlacklists()}
rowKey="id"
loading={loading}
rowSelection={{
selectedRowKeys,
onChange: setSelectedRowKeys,
getCheckboxProps: (record) => ({
disabled: record.status !== 'ACTIVE',
}),
}}
pagination={{ pageSize: 10 }}
/>
</Card>
<Modal
title="Add to Blacklist"
open={createModalVisible}
onCancel={() => setCreateModalVisible(false)}
footer={null}
width={700}
>
<Form form={form} layout="vertical" onFinish={handleCreate}>
<Row gutter={16}>
<Col span={12}>
<Form.Item name="platform" label="Platform" rules={[{ required: true }]}>
<Select placeholder="Select Platform">
{PLATFORMS.map(platform => (
<Option key={platform} value={platform}>{platform}</Option>
))}
</Select>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name="platform_buyer_id" label="Platform Buyer ID" rules={[{ required: true }]}>
<Input placeholder="Enter platform buyer ID" />
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col span={12}>
<Form.Item name="buyer_name" label="Buyer Name" rules={[{ required: true }]}>
<Input placeholder="Enter buyer name" />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name="buyer_email" label="Buyer Email" rules={[{ required: true, type: 'email' }]}>
<Input placeholder="Enter buyer email" />
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col span={12}>
<Form.Item name="buyer_phone" label="Buyer Phone">
<Input placeholder="Enter buyer phone" />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name="blacklist_type" label="Blacklist Type" rules={[{ required: true }]}>
<Select placeholder="Select Type">
{BLACKLIST_TYPES.map(type => (
<Option key={type.value} value={type.value}>{type.label}</Option>
))}
</Select>
</Form.Item>
</Col>
</Row>
<Form.Item name="blacklist_reason" label="Reason" rules={[{ required: true }]}>
<TextArea rows={3} placeholder="Enter reason for blacklisting" />
</Form.Item>
<Row gutter={16}>
<Col span={12}>
<Form.Item name="risk_score" label="Risk Score" rules={[{ required: true }]}>
<InputNumber min={0} max={100} style={{ width: '100%' }} placeholder="0-100" />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name="expiry_date" label="Expiry Date">
<DatePicker style={{ width: '100%' }} />
</Form.Item>
</Col>
</Row>
<Form.Item name="evidence" label="Evidence">
<Upload>
<Button icon={<UploadOutlined />}>Upload Evidence</Button>
</Upload>
</Form.Item>
<div style={{ textAlign: 'right' }}>
<Button onClick={() => setCreateModalVisible(false)} style={{ marginRight: 8 }}>Cancel</Button>
<Button type="primary" htmlType="submit" loading={loading}>Add to Blacklist</Button>
</div>
</Form>
</Modal>
<Modal
title="Edit Blacklist Record"
open={editModalVisible}
onCancel={() => {
setEditModalVisible(false);
setSelectedBlacklist(null);
form.resetFields();
}}
footer={null}
width={700}
>
<Form form={form} layout="vertical" onFinish={handleEdit}>
<Row gutter={16}>
<Col span={12}>
<Form.Item name="platform" label="Platform" rules={[{ required: true }]}>
<Select placeholder="Select Platform">
{PLATFORMS.map(platform => (
<Option key={platform} value={platform}>{platform}</Option>
))}
</Select>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name="platform_buyer_id" label="Platform Buyer ID" rules={[{ required: true }]}>
<Input placeholder="Enter platform buyer ID" />
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col span={12}>
<Form.Item name="buyer_name" label="Buyer Name" rules={[{ required: true }]}>
<Input placeholder="Enter buyer name" />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name="buyer_email" label="Buyer Email" rules={[{ required: true, type: 'email' }]}>
<Input placeholder="Enter buyer email" />
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col span={12}>
<Form.Item name="buyer_phone" label="Buyer Phone">
<Input placeholder="Enter buyer phone" />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name="blacklist_type" label="Blacklist Type" rules={[{ required: true }]}>
<Select placeholder="Select Type">
{BLACKLIST_TYPES.map(type => (
<Option key={type.value} value={type.value}>{type.label}</Option>
))}
</Select>
</Form.Item>
</Col>
</Row>
<Form.Item name="blacklist_reason" label="Reason" rules={[{ required: true }]}>
<TextArea rows={3} placeholder="Enter reason for blacklisting" />
</Form.Item>
<Row gutter={16}>
<Col span={8}>
<Form.Item name="risk_score" label="Risk Score" rules={[{ required: true }]}>
<InputNumber min={0} max={100} style={{ width: '100%' }} placeholder="0-100" />
</Form.Item>
</Col>
<Col span={8}>
<Form.Item name="expiry_date" label="Expiry Date">
<DatePicker style={{ width: '100%' }} />
</Form.Item>
</Col>
<Col span={8}>
<Form.Item name="status" label="Status" rules={[{ required: true }]}>
<Select placeholder="Select Status">
<Option value="ACTIVE">Active</Option>
<Option value="INACTIVE">Inactive</Option>
<Option value="EXPIRED">Expired</Option>
</Select>
</Form.Item>
</Col>
</Row>
<Form.Item name="evidence" label="Evidence">
<Upload>
<Button icon={<UploadOutlined />}>Upload Evidence</Button>
</Upload>
</Form.Item>
<div style={{ textAlign: 'right' }}>
<Button onClick={() => {
setEditModalVisible(false);
setSelectedBlacklist(null);
form.resetFields();
}} style={{ marginRight: 8 }}>Cancel</Button>
<Button type="primary" htmlType="submit" loading={loading}>Update Record</Button>
</div>
</Form>
</Modal>
<Modal
title="Blacklist Record Details"
open={detailModalVisible}
onCancel={() => setDetailModalVisible(false)}
footer={[
<Button key="close" onClick={() => setDetailModalVisible(false)}>Close</Button>,
selectedBlacklist?.status === 'ACTIVE' && (
<Button key="edit" type="primary" icon={<EditOutlined />} onClick={() => {
setDetailModalVisible(false);
handleEditClick(selectedBlacklist!);
}}>
Edit Record
</Button>
),
]}
width={700}
>
{selectedBlacklist && (
<>
<Descriptions bordered column={2}>
<Descriptions.Item label="Buyer Name" span={2}>{selectedBlacklist.buyer_name}</Descriptions.Item>
<Descriptions.Item label="Email">{selectedBlacklist.buyer_email}</Descriptions.Item>
<Descriptions.Item label="Phone">{selectedBlacklist.buyer_phone || '-'}</Descriptions.Item>
<Descriptions.Item label="Platform">{selectedBlacklist.platform}</Descriptions.Item>
<Descriptions.Item label="Platform Buyer ID">{selectedBlacklist.platform_buyer_id}</Descriptions.Item>
<Descriptions.Item label="Blacklist Type">
<Tag color={BLACKLIST_TYPES.find(t => t.value === selectedBlacklist.blacklist_type)?.color}>
{BLACKLIST_TYPES.find(t => t.value === selectedBlacklist.blacklist_type)?.label}
</Tag>
</Descriptions.Item>
<Descriptions.Item label="Risk Score">
<Tag color={getRiskLevel(selectedBlacklist.risk_score).color}>
{selectedBlacklist.risk_score} - {getRiskLevel(selectedBlacklist.risk_score).level}
</Tag>
</Descriptions.Item>
<Descriptions.Item label="Status">
<Tag color={STATUS_CONFIG[selectedBlacklist.status].color} icon={STATUS_CONFIG[selectedBlacklist.status].icon}>
{STATUS_CONFIG[selectedBlacklist.status].text}
</Tag>
</Descriptions.Item>
<Descriptions.Item label="Blacklist Date">{selectedBlacklist.blacklist_date}</Descriptions.Item>
<Descriptions.Item label="Expiry Date">{selectedBlacklist.expiry_date || '-'}</Descriptions.Item>
<Descriptions.Item label="Reason" span={2}>{selectedBlacklist.blacklist_reason}</Descriptions.Item>
<Descriptions.Item label="Evidence" span={2}>{selectedBlacklist.evidence || '-'}</Descriptions.Item>
<Descriptions.Item label="Created By">{selectedBlacklist.created_by}</Descriptions.Item>
<Descriptions.Item label="Created At">{selectedBlacklist.created_at}</Descriptions.Item>
</Descriptions>
</>
)}
</Modal>
</div>
);
};
export default BlacklistManage;

View File

@@ -0,0 +1,705 @@
import React, { useState, useEffect } from 'react';
import {
Card,
Table,
Button,
Space,
Modal,
Form,
Select,
DatePicker,
Descriptions,
Divider,
message,
Tag,
Tabs,
Row,
Col,
Statistic,
Alert,
Tooltip,
Progress,
Badge,
List,
Empty,
} from 'antd';
import {
EyeOutlined,
SearchOutlined,
WarningOutlined,
CheckCircleOutlined,
CloseCircleOutlined,
ExclamationCircleOutlined,
ThunderboltOutlined,
SafetyOutlined,
RadarChartOutlined,
LineChartOutlined,
BarChartOutlined,
DashboardOutlined,
FilterOutlined,
ReloadOutlined,
} from '@ant-design/icons';
import type { ColumnsType } from 'antd/es/table';
import dayjs from 'dayjs';
const { Option } = Select;
const { TabPane } = Tabs;
const { RangePicker } = DatePicker;
interface RiskAssessment {
id: string;
tenant_id: string;
shop_id: string;
buyer_id: string;
platform: string;
platform_buyer_id: string;
risk_score: number;
risk_level: 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL';
risk_factors: string[];
recommendations: string[];
is_blacklisted: boolean;
blacklist_reasons?: string[];
confidence_score: number;
assessment_reason: string;
assessment_date: string;
created_at: string;
}
interface RiskStatistics {
total_assessments: number;
high_risk_count: number;
medium_risk_count: number;
low_risk_count: number;
critical_risk_count: number;
blacklist_conversion_rate: number;
false_positives: number;
false_negatives: number;
by_platform: Record<string, number>;
by_risk_level: Record<string, number>;
average_risk_score: number;
risk_trend: {
date: string;
average_score: number;
}[];
}
const RISK_LEVEL_CONFIG: Record<string, { color: string; text: string; icon: React.ReactNode; percentage: number }> = {
LOW: { color: 'success', text: 'Low', icon: <CheckCircleOutlined />, percentage: 25 },
MEDIUM: { color: 'warning', text: 'Medium', icon: <ExclamationCircleOutlined />, percentage: 50 },
HIGH: { color: 'error', text: 'High', icon: <WarningOutlined />, percentage: 75 },
CRITICAL: { color: 'red', text: 'Critical', icon: <CloseCircleOutlined />, percentage: 100 },
};
const PLATFORMS = [
'Amazon',
'eBay',
'Shopify',
'Walmart',
'TikTok Shop',
'Temu',
'Alibaba',
'Other',
];
const MOCK_ASSESSMENTS: RiskAssessment[] = [
{
id: '1',
tenant_id: 'tenant-001',
shop_id: 'shop-001',
buyer_id: 'buyer-001',
platform: 'Amazon',
platform_buyer_id: 'AMZ-001',
risk_score: 85,
risk_level: 'CRITICAL',
risk_factors: ['High return rate', 'Multiple chargebacks', 'Suspicious activity'],
recommendations: ['Block buyer immediately', 'Add to blacklist', 'Review all past transactions'],
is_blacklisted: true,
blacklist_reasons: ['Fraudulent transactions'],
confidence_score: 85,
assessment_reason: 'New order received',
assessment_date: '2026-03-18 10:30:00',
created_at: '2026-03-18 10:30:00',
},
{
id: '2',
tenant_id: 'tenant-001',
shop_id: 'shop-001',
buyer_id: 'buyer-002',
platform: 'eBay',
platform_buyer_id: 'EBAY-002',
risk_score: 72,
risk_level: 'HIGH',
risk_factors: ['High return rate', 'New account'],
recommendations: ['Place order on hold', 'Request additional verification', 'Limit order amount'],
is_blacklisted: false,
confidence_score: 78,
assessment_reason: 'New order received',
assessment_date: '2026-03-18 11:15:00',
created_at: '2026-03-18 11:15:00',
},
{
id: '3',
tenant_id: 'tenant-001',
shop_id: 'shop-001',
buyer_id: 'buyer-003',
platform: 'Amazon',
platform_buyer_id: 'AMZ-003',
risk_score: 45,
risk_level: 'MEDIUM',
risk_factors: ['Moderate return rate'],
recommendations: ['Monitor transaction closely', 'Set lower order limits'],
is_blacklisted: false,
confidence_score: 65,
assessment_reason: 'New order received',
assessment_date: '2026-03-18 12:00:00',
created_at: '2026-03-18 12:00:00',
},
{
id: '4',
tenant_id: 'tenant-001',
shop_id: 'shop-001',
buyer_id: 'buyer-004',
platform: 'Shopify',
platform_buyer_id: 'SHOPIFY-004',
risk_score: 15,
risk_level: 'LOW',
risk_factors: [],
recommendations: ['Process order normally', 'Continue monitoring'],
is_blacklisted: false,
confidence_score: 90,
assessment_reason: 'New order received',
assessment_date: '2026-03-18 12:30:00',
created_at: '2026-03-18 12:30:00',
},
{
id: '5',
tenant_id: 'tenant-001',
shop_id: 'shop-001',
buyer_id: 'buyer-005',
platform: 'Walmart',
platform_buyer_id: 'WMT-005',
risk_score: 65,
risk_level: 'HIGH',
risk_factors: ['Chargeback history', 'Multiple IP changes'],
recommendations: ['Place order on hold', 'Use secure payment methods', 'Monitor future transactions'],
is_blacklisted: true,
blacklist_reasons: ['Excessive chargebacks'],
confidence_score: 72,
assessment_reason: 'New order received',
assessment_date: '2026-03-18 13:00:00',
created_at: '2026-03-18 13:00:00',
},
];
const MOCK_STATISTICS: RiskStatistics = {
total_assessments: 150,
high_risk_count: 35,
medium_risk_count: 60,
low_risk_count: 45,
critical_risk_count: 10,
blacklist_conversion_rate: 12.5,
false_positives: 3,
false_negatives: 1,
by_platform: {
'Amazon': 60,
'eBay': 35,
'Shopify': 25,
'Walmart': 15,
'TikTok Shop': 10,
'Temu': 5,
},
by_risk_level: {
'LOW': 45,
'MEDIUM': 60,
'HIGH': 35,
'CRITICAL': 10,
},
average_risk_score: 42.5,
risk_trend: [
{ date: '2026-03-12', average_score: 38.2 },
{ date: '2026-03-13', average_score: 40.5 },
{ date: '2026-03-14', average_score: 39.8 },
{ date: '2026-03-15', average_score: 41.2 },
{ date: '2026-03-16', average_score: 43.5 },
{ date: '2026-03-17', average_score: 42.1 },
{ date: '2026-03-18', average_score: 42.5 },
],
};
const RiskMonitor: React.FC = () => {
const [loading, setLoading] = useState(false);
const [assessments, setAssessments] = useState<RiskAssessment[]>(MOCK_ASSESSMENTS);
const [statistics, setStatistics] = useState<RiskStatistics>(MOCK_STATISTICS);
const [detailModalVisible, setDetailModalVisible] = useState(false);
const [selectedAssessment, setSelectedAssessment] = useState<RiskAssessment | null>(null);
const [activeTab, setActiveTab] = useState('all');
const [platformFilter, setPlatformFilter] = useState<string>('');
const [riskLevelFilter, setRiskLevelFilter] = useState<string>('');
const [dateRange, setDateRange] = useState<[dayjs.Dayjs, dayjs.Dayjs] | null>(null);
useEffect(() => {
fetchRiskData();
}, []);
const fetchRiskData = async () => {
setLoading(true);
try {
setAssessments(MOCK_ASSESSMENTS);
setStatistics(MOCK_STATISTICS);
} finally {
setLoading(false);
}
};
const handleViewDetail = (assessment: RiskAssessment) => {
setSelectedAssessment(assessment);
setDetailModalVisible(true);
};
const getFilteredAssessments = () => {
let filtered = assessments;
if (activeTab !== 'all') {
filtered = filtered.filter(a => a.risk_level === activeTab.toUpperCase());
}
if (platformFilter) {
filtered = filtered.filter(a => a.platform === platformFilter);
}
if (riskLevelFilter) {
filtered = filtered.filter(a => a.risk_level === riskLevelFilter.toUpperCase());
}
if (dateRange) {
filtered = filtered.filter(a => {
const assessmentDate = dayjs(a.assessment_date);
return assessmentDate.isAfter(dateRange[0].startOf('day')) &&
assessmentDate.isBefore(dateRange[1].endOf('day'));
});
}
return filtered;
};
const getRiskColor = (score: number): string => {
if (score >= 80) return '#ff4d4f';
if (score >= 60) return '#ff7a45';
if (score >= 40) return '#faad14';
return '#52c41a';
};
const columns: ColumnsType<RiskAssessment> = [
{
title: 'Platform',
dataIndex: 'platform',
key: 'platform',
width: 100,
},
{
title: 'Platform Buyer ID',
dataIndex: 'platform_buyer_id',
key: 'platform_buyer_id',
width: 150,
},
{
title: 'Risk Score',
dataIndex: 'risk_score',
key: 'risk_score',
width: 150,
render: (score: number, record: RiskAssessment) => (
<div>
<Progress
percent={score}
strokeColor={getRiskColor(score)}
size="small"
format={percent => `${percent}`}
/>
<Tag color={getRiskColor(score)} style={{ marginTop: 4 }}>
{record.risk_level}
</Tag>
</div>
),
sorter: (a, b) => a.risk_score - b.risk_score,
},
{
title: 'Risk Factors',
dataIndex: 'risk_factors',
key: 'risk_factors',
width: 200,
render: (factors: string[]) => (
<div>
{factors.slice(0, 2).map((factor, index) => (
<Tag key={index} color="orange" style={{ marginBottom: 2 }}>
{factor}
</Tag>
))}
{factors.length > 2 && (
<Tooltip title={factors.slice(2).join(', ')}>
<Tag color="default">+{factors.length - 2} more</Tag>
</Tooltip>
)}
</div>
),
},
{
title: 'Blacklisted',
dataIndex: 'is_blacklisted',
key: 'is_blacklisted',
width: 100,
render: (isBlacklisted: boolean) => (
<Tag color={isBlacklisted ? 'error' : 'success'} icon={isBlacklisted ? <CloseCircleOutlined /> : <CheckCircleOutlined />}>
{isBlacklisted ? 'Yes' : 'No'}
</Tag>
),
},
{
title: 'Confidence',
dataIndex: 'confidence_score',
key: 'confidence_score',
width: 100,
render: (score: number) => (
<Progress
percent={score}
strokeColor="#1890ff"
size="small"
format={percent => `${percent}%`}
/>
),
},
{
title: 'Assessment Date',
dataIndex: 'assessment_date',
key: 'assessment_date',
width: 150,
render: (date: string) => dayjs(date).format('YYYY-MM-DD HH:mm'),
},
{
title: 'Actions',
key: 'actions',
width: 80,
render: (_, record: RiskAssessment) => (
<Tooltip title="View Details">
<Button type="link" icon={<EyeOutlined />} onClick={() => handleViewDetail(record)} />
</Tooltip>
),
},
];
const highRiskAlerts = assessments.filter(a => a.risk_level === 'HIGH' || a.risk_level === 'CRITICAL');
return (
<div className="risk-monitor-page">
<Row gutter={16} style={{ marginBottom: 24 }}>
<Col span={4}>
<Card>
<Statistic
title="Total Assessments"
value={statistics.total_assessments}
prefix={<DashboardOutlined />}
/>
</Card>
</Col>
<Col span={4}>
<Card>
<Statistic
title="Critical Risk"
value={statistics.critical_risk_count}
valueStyle={{ color: '#ff4d4f' }}
prefix={<CloseCircleOutlined />}
/>
</Card>
</Col>
<Col span={4}>
<Card>
<Statistic
title="High Risk"
value={statistics.high_risk_count}
valueStyle={{ color: '#ff7a45' }}
prefix={<WarningOutlined />}
/>
</Card>
</Col>
<Col span={4}>
<Card>
<Statistic
title="Medium Risk"
value={statistics.medium_risk_count}
valueStyle={{ color: '#faad14' }}
prefix={<ExclamationCircleOutlined />}
/>
</Card>
</Col>
<Col span={4}>
<Card>
<Statistic
title="Low Risk"
value={statistics.low_risk_count}
valueStyle={{ color: '#52c41a' }}
prefix={<CheckCircleOutlined />}
/>
</Card>
</Col>
<Col span={4}>
<Card>
<Statistic
title="Avg Risk Score"
value={statistics.average_risk_score}
precision={1}
valueStyle={{ color: '#1890ff' }}
prefix={<RadarChartOutlined />}
/>
</Card>
</Col>
</Row>
<Row gutter={16} style={{ marginBottom: 24 }}>
<Col span={12}>
<Card title="Risk Distribution" extra={<BarChartOutlined />}>
<div style={{ marginTop: 16 }}>
{Object.entries(statistics.by_risk_level).map(([level, count]) => {
const config = RISK_LEVEL_CONFIG[level];
const percentage = statistics.total_assessments > 0
? (count / statistics.total_assessments) * 100
: 0;
return (
<div key={level} style={{ marginBottom: 16 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 4 }}>
<span>
<Tag color={config.color} icon={config.icon}>{config.text}</Tag>
<span style={{ marginLeft: 8 }}>{count} assessments</span>
</span>
<span>{percentage.toFixed(1)}%</span>
</div>
<Progress
percent={percentage}
strokeColor={config.color}
showInfo={false}
/>
</div>
);
})}
</div>
</Card>
</Col>
<Col span={12}>
<Card title="Risk by Platform" extra={<ThunderboltOutlined />}>
<List
dataSource={Object.entries(statistics.by_platform)
.sort(([, a], [, b]) => b - a)
.slice(0, 6)}
renderItem={([platform, count]) => (
<List.Item key={platform}>
<List.Item.Meta
title={platform}
description={`${count} assessments`}
/>
<Progress
percent={statistics.total_assessments > 0 ? (count / statistics.total_assessments) * 100 : 0}
strokeColor="#1890ff"
style={{ width: 150 }}
showInfo={false}
/>
</List.Item>
)}
/>
</Card>
</Col>
</Row>
{highRiskAlerts.length > 0 && (
<Card
title={
<Space>
<WarningOutlined style={{ color: '#ff4d4f' }} />
<span>High Risk Alerts</span>
<Badge count={highRiskAlerts.length} style={{ backgroundColor: '#ff4d4f' }} />
</Space>
}
style={{ marginBottom: 24 }}
>
<Alert
message={`${highRiskAlerts.length} buyers identified as high or critical risk`}
description="Immediate action recommended for critical risk buyers"
type="error"
showIcon
style={{ marginBottom: 16 }}
/>
<List
dataSource={highRiskAlerts.slice(0, 5)}
renderItem={(assessment) => (
<List.Item
actions={[
<Button type="link" icon={<EyeOutlined />} onClick={() => handleViewDetail(assessment)}>
View Details
</Button>
]}
>
<List.Item.Meta
title={
<Space>
<span>{assessment.platform}</span>
<span>{assessment.platform_buyer_id}</span>
<Tag color={getRiskColor(assessment.risk_score)}>{assessment.risk_level}</Tag>
</Space>
}
description={`Risk Score: ${assessment.risk_score} | ${dayjs(assessment.assessment_date).format('YYYY-MM-DD HH:mm')}`}
/>
</List.Item>
)}
/>
</Card>
)}
<Card
title="Risk Assessment History"
extra={
<Space>
<Button icon={<ReloadOutlined />} onClick={fetchRiskData}>Refresh</Button>
</Space>
}
>
<div style={{ display: 'flex', marginBottom: 16, gap: 16, flexWrap: 'wrap' }}>
<Select
placeholder="Filter by Platform"
style={{ width: 150 }}
onChange={setPlatformFilter}
allowClear
suffixIcon={<FilterOutlined />}
>
{PLATFORMS.map(platform => (
<Option key={platform} value={platform}>{platform}</Option>
))}
</Select>
<Select
placeholder="Filter by Risk Level"
style={{ width: 150 }}
onChange={setRiskLevelFilter}
allowClear
suffixIcon={<FilterOutlined />}
>
{Object.entries(RISK_LEVEL_CONFIG).map(([level, config]) => (
<Option key={level} value={level}>
<Tag color={config.color} icon={config.icon}>{config.text}</Tag>
</Option>
))}
</Select>
<RangePicker
style={{ width: 300 }}
onChange={(dates) => setDateRange(dates as [dayjs.Dayjs, dayjs.Dayjs] | null)}
/>
</div>
<Tabs activeKey={activeTab} onChange={setActiveTab}>
<TabPane tab="All" key="all" />
<TabPane tab={<Badge count={statistics.critical_risk_count} offset={[10, 0]} size="small">Critical</Badge>} key="critical" />
<TabPane tab={<Badge count={statistics.high_risk_count} offset={[10, 0]} size="small">High</Badge>} key="high" />
<TabPane tab="Medium" key="medium" />
<TabPane tab="Low" key="low" />
</Tabs>
<Table
columns={columns}
dataSource={getFilteredAssessments()}
rowKey="id"
loading={loading}
pagination={{ pageSize: 10 }}
locale={{
emptyText: (
<Empty
image={Empty.PRESENTED_IMAGE_SIMPLE}
description="No risk assessments found"
/>
),
}}
/>
</Card>
<Modal
title="Risk Assessment Details"
open={detailModalVisible}
onCancel={() => setDetailModalVisible(false)}
footer={[
<Button key="close" onClick={() => setDetailModalVisible(false)}>Close</Button>,
]}
width={800}
>
{selectedAssessment && (
<>
<Descriptions bordered column={2}>
<Descriptions.Item label="Platform">{selectedAssessment.platform}</Descriptions.Item>
<Descriptions.Item label="Platform Buyer ID">{selectedAssessment.platform_buyer_id}</Descriptions.Item>
<Descriptions.Item label="Risk Score">
<Progress
percent={selectedAssessment.risk_score}
strokeColor={getRiskColor(selectedAssessment.risk_score)}
format={percent => `${percent}`}
/>
</Descriptions.Item>
<Descriptions.Item label="Risk Level">
<Tag color={getRiskColor(selectedAssessment.risk_score)} icon={RISK_LEVEL_CONFIG[selectedAssessment.risk_level].icon}>
{RISK_LEVEL_CONFIG[selectedAssessment.risk_level].text}
</Tag>
</Descriptions.Item>
<Descriptions.Item label="Confidence Score">
<Progress
percent={selectedAssessment.confidence_score}
strokeColor="#1890ff"
format={percent => `${percent}%`}
/>
</Descriptions.Item>
<Descriptions.Item label="Blacklisted">
<Tag color={selectedAssessment.is_blacklisted ? 'error' : 'success'} icon={selectedAssessment.is_blacklisted ? <CloseCircleOutlined /> : <CheckCircleOutlined />}>
{selectedAssessment.is_blacklisted ? 'Yes' : 'No'}
</Tag>
</Descriptions.Item>
<Descriptions.Item label="Assessment Reason" span={2}>{selectedAssessment.assessment_reason}</Descriptions.Item>
<Descriptions.Item label="Assessment Date" span={2}>
{dayjs(selectedAssessment.assessment_date).format('YYYY-MM-DD HH:mm:ss')}
</Descriptions.Item>
</Descriptions>
<Divider>Risk Factors</Divider>
{selectedAssessment.risk_factors.length > 0 ? (
<Space wrap>
{selectedAssessment.risk_factors.map((factor, index) => (
<Tag key={index} color="orange">{factor}</Tag>
))}
</Space>
) : (
<Empty description="No risk factors identified" image={Empty.PRESENTED_IMAGE_SIMPLE} />
)}
<Divider>Recommendations</Divider>
{selectedAssessment.recommendations.length > 0 ? (
<List
dataSource={selectedAssessment.recommendations}
renderItem={(recommendation, index) => (
<List.Item key={index}>
<SafetyOutlined style={{ color: '#1890ff', marginRight: 8 }} />
{recommendation}
</List.Item>
)}
/>
) : (
<Empty description="No recommendations" image={Empty.PRESENTED_IMAGE_SIMPLE} />
)}
{selectedAssessment.is_blacklisted && selectedAssessment.blacklist_reasons && (
<>
<Divider>Blacklist Reasons</Divider>
<Space wrap>
{selectedAssessment.blacklist_reasons.map((reason, index) => (
<Tag key={index} color="error">{reason}</Tag>
))}
</Space>
</>
)}
</>
)}
</Modal>
</div>
);
};
export default RiskMonitor;

View File

@@ -0,0 +1,237 @@
import React, { useState, useEffect } from 'react';
import { Table, Input, Button, Select, DatePicker, message, Card, Typography } from 'antd';
import { SearchOutlined, PlusOutlined, EditOutlined, DeleteOutlined, EyeOutlined } from '@ant-design/icons';
import { getMerchants, createMerchant, updateMerchant, deleteMerchant } from '../../services/merchantService';
const { Option } = Select;
const { Title, Text } = Typography;
interface Merchant {
id: string;
tenantId: string;
companyName: string;
businessLicense: string;
contactEmail: string;
contactPhone: string;
status: 'PENDING' | 'ACTIVE' | 'SUSPENDED' | 'TERMINATED';
tier: 'BASIC' | 'PRO' | 'ENTERPRISE';
traceId: string;
createdAt: string;
updatedAt: string;
}
const MerchantManage: React.FC = () => {
const [merchants, setMerchants] = useState<Merchant[]>([]);
const [loading, setLoading] = useState(false);
const [searchText, setSearchText] = useState('');
const [statusFilter, setStatusFilter] = useState<string>('');
const [tierFilter, setTierFilter] = useState<string>('');
const fetchMerchants = async () => {
setLoading(true);
try {
const response = await getMerchants();
setMerchants(response.data);
} catch (error) {
message.error('获取商户列表失败');
console.error('获取商户列表失败:', error);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchMerchants();
}, []);
const handleSearch = (value: string) => {
setSearchText(value);
};
const handleStatusFilter = (value: string) => {
setStatusFilter(value);
};
const handleTierFilter = (value: string) => {
setTierFilter(value);
};
const filteredMerchants = merchants.filter(merchant => {
const matchesSearch = merchant.companyName.toLowerCase().includes(searchText.toLowerCase()) ||
merchant.contactEmail.toLowerCase().includes(searchText.toLowerCase());
const matchesStatus = statusFilter ? merchant.status === statusFilter : true;
const matchesTier = tierFilter ? merchant.tier === tierFilter : true;
return matchesSearch && matchesStatus && matchesTier;
});
const handleCreate = () => {
// 跳转到创建商户页面或打开创建弹窗
message.info('创建商户功能待实现');
};
const handleEdit = (merchant: Merchant) => {
// 跳转到编辑商户页面或打开编辑弹窗
message.info(`编辑商户: ${merchant.companyName}`);
};
const handleDelete = (merchantId: string) => {
// 确认删除后调用API
message.info(`删除商户: ${merchantId}`);
};
const handleView = (merchant: Merchant) => {
// 跳转到商户详情页面
message.info(`查看商户详情: ${merchant.companyName}`);
};
const columns = [
{
title: '商户ID',
dataIndex: 'id',
key: 'id',
ellipsis: true,
},
{
title: '公司名称',
dataIndex: 'companyName',
key: 'companyName',
ellipsis: true,
},
{
title: '联系邮箱',
dataIndex: 'contactEmail',
key: 'contactEmail',
ellipsis: true,
},
{
title: '联系电话',
dataIndex: 'contactPhone',
key: 'contactPhone',
ellipsis: true,
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
render: (status: string) => {
const statusMap: Record<string, string> = {
PENDING: '待审核',
ACTIVE: '活跃',
SUSPENDED: '暂停',
TERMINATED: '终止',
};
return <Text>{statusMap[status] || status}</Text>;
},
},
{
title: '等级',
dataIndex: 'tier',
key: 'tier',
render: (tier: string) => {
const tierMap: Record<string, string> = {
BASIC: '基础版',
PRO: '专业版',
ENTERPRISE: '企业版',
};
return <Text>{tierMap[tier] || tier}</Text>;
},
},
{
title: '创建时间',
dataIndex: 'createdAt',
key: 'createdAt',
ellipsis: true,
},
{
title: '操作',
key: 'action',
render: (_: any, merchant: Merchant) => (
<div>
<Button
icon={<EyeOutlined />}
size="small"
style={{ marginRight: 8 }}
onClick={() => handleView(merchant)}
>
</Button>
<Button
icon={<EditOutlined />}
size="small"
style={{ marginRight: 8 }}
onClick={() => handleEdit(merchant)}
>
</Button>
<Button
icon={<DeleteOutlined />}
size="small"
danger
onClick={() => handleDelete(merchant.id)}
>
</Button>
</div>
),
},
];
return (
<Card>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 24 }}>
<Title level={4}></Title>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={handleCreate}
>
</Button>
</div>
<div style={{ display: 'flex', marginBottom: 16, gap: 16 }}>
<Input
placeholder="搜索商户名称或邮箱"
prefix={<SearchOutlined />}
style={{ width: 300 }}
onChange={(e) => handleSearch(e.target.value)}
/>
<Select
placeholder="筛选状态"
style={{ width: 120 }}
onChange={handleStatusFilter}
allowClear
>
<Option value="PENDING"></Option>
<Option value="ACTIVE"></Option>
<Option value="SUSPENDED"></Option>
<Option value="TERMINATED"></Option>
</Select>
<Select
placeholder="筛选等级"
style={{ width: 120 }}
onChange={handleTierFilter}
allowClear
>
<Option value="BASIC"></Option>
<Option value="PRO"></Option>
<Option value="ENTERPRISE"></Option>
</Select>
</div>
<Table
columns={columns}
dataSource={filteredMerchants}
rowKey="id"
loading={loading}
pagination={{
showSizeChanger: true,
pageSizeOptions: ['10', '20', '50'],
defaultPageSize: 10,
}}
/>
</Card>
);
};
export default MerchantManage;

View File

@@ -0,0 +1,273 @@
import React, { useState, useEffect } from 'react';
import { Table, Input, Button, Select, DatePicker, message, Card, Typography, Tag } from 'antd';
import { SearchOutlined, EyeOutlined, ReloadOutlined } from '@ant-design/icons';
import { getMerchantOrders, updateOrderStatus } from '../../services/merchantOrderService';
const { Option } = Select;
const { Title, Text } = Typography;
const { RangePicker } = DatePicker;
interface Order {
id: string;
merchantId: string;
merchantName: string;
orderId: string;
platform: string;
totalAmount: number;
status: 'PENDING' | 'PROCESSING' | 'SHIPPED' | 'DELIVERED' | 'CANCELLED' | 'REFUNDED';
createdAt: string;
updatedAt: string;
}
const MerchantOrderManage: React.FC = () => {
const [orders, setOrders] = useState<Order[]>([]);
const [merchants, setMerchants] = useState<{ id: string; companyName: string }[]>([]);
const [loading, setLoading] = useState(false);
const [searchText, setSearchText] = useState('');
const [merchantFilter, setMerchantFilter] = useState<string>('');
const [platformFilter, setPlatformFilter] = useState<string>('');
const [statusFilter, setStatusFilter] = useState<string>('');
const [dateRange, setDateRange] = useState<[string, string] | null>(null);
const fetchOrders = async () => {
setLoading(true);
try {
const response = await getMerchantOrders();
setOrders(response.data);
} catch (error) {
message.error('获取订单列表失败');
console.error('获取订单列表失败:', error);
} finally {
setLoading(false);
}
};
const fetchMerchants = async () => {
try {
// 实际项目中应该调用获取商户列表的API
// 这里模拟数据
setMerchants([
{ id: '1', companyName: '商户A' },
{ id: '2', companyName: '商户B' },
{ id: '3', companyName: '商户C' },
]);
} catch (error) {
console.error('获取商户列表失败:', error);
}
};
useEffect(() => {
fetchMerchants();
fetchOrders();
}, []);
const handleSearch = (value: string) => {
setSearchText(value);
};
const handleMerchantFilter = (value: string) => {
setMerchantFilter(value);
};
const handlePlatformFilter = (value: string) => {
setPlatformFilter(value);
};
const handleStatusFilter = (value: string) => {
setStatusFilter(value);
};
const handleDateRangeChange = (dates: any) => {
if (dates) {
setDateRange([dates[0].format('YYYY-MM-DD'), dates[1].format('YYYY-MM-DD')]);
} else {
setDateRange(null);
}
};
const filteredOrders = orders.filter(order => {
const matchesSearch = order.orderId.includes(searchText) ||
order.merchantName.toLowerCase().includes(searchText.toLowerCase());
const matchesMerchant = merchantFilter ? order.merchantId === merchantFilter : true;
const matchesPlatform = platformFilter ? order.platform === platformFilter : true;
const matchesStatus = statusFilter ? order.status === statusFilter : true;
return matchesSearch && matchesMerchant && matchesPlatform && matchesStatus;
});
const handleView = (order: Order) => {
// 跳转到订单详情页面
message.info(`查看订单详情: ${order.orderId}`);
};
const handleStatusUpdate = (order: Order, newStatus: Order['status']) => {
// 调用API更新订单状态
message.info(`更新订单 ${order.orderId} 状态为: ${newStatus}`);
};
const handleRefresh = () => {
fetchOrders();
};
const getStatusTag = (status: string) => {
const statusConfig: Record<string, { color: string; text: string }> = {
PENDING: { color: 'blue', text: '待处理' },
PROCESSING: { color: 'orange', text: '处理中' },
SHIPPED: { color: 'purple', text: '已发货' },
DELIVERED: { color: 'green', text: '已送达' },
CANCELLED: { color: 'gray', text: '已取消' },
REFUNDED: { color: 'red', text: '已退款' },
};
const config = statusConfig[status] || { color: 'default', text: status };
return <Tag color={config.color}>{config.text}</Tag>;
};
const columns = [
{
title: '订单ID',
dataIndex: 'id',
key: 'id',
ellipsis: true,
},
{
title: '平台订单号',
dataIndex: 'orderId',
key: 'orderId',
ellipsis: true,
},
{
title: '商户',
dataIndex: 'merchantName',
key: 'merchantName',
ellipsis: true,
},
{
title: '平台',
dataIndex: 'platform',
key: 'platform',
ellipsis: true,
},
{
title: '总金额',
dataIndex: 'totalAmount',
key: 'totalAmount',
render: (amount: number) => <Text>${amount.toFixed(2)}</Text>,
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
render: (status: string) => getStatusTag(status),
},
{
title: '创建时间',
dataIndex: 'createdAt',
key: 'createdAt',
ellipsis: true,
},
{
title: '操作',
key: 'action',
render: (_: any, order: Order) => (
<div>
<Button
icon={<EyeOutlined />}
size="small"
style={{ marginRight: 8 }}
onClick={() => handleView(order)}
>
</Button>
<Select
defaultValue={order.status}
style={{ width: 120 }}
onChange={(value) => handleStatusUpdate(order, value)}
>
<Option value="PENDING"></Option>
<Option value="PROCESSING"></Option>
<Option value="SHIPPED"></Option>
<Option value="DELIVERED"></Option>
<Option value="CANCELLED"></Option>
<Option value="REFUNDED">退</Option>
</Select>
</div>
),
},
];
return (
<Card>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 24 }}>
<Title level={4}></Title>
<Button
icon={<ReloadOutlined />}
onClick={handleRefresh}
>
</Button>
</div>
<div style={{ display: 'flex', marginBottom: 16, gap: 16, flexWrap: 'wrap' }}>
<Input
placeholder="搜索订单号或商户名称"
prefix={<SearchOutlined />}
style={{ width: 300 }}
onChange={(e) => handleSearch(e.target.value)}
/>
<Select
placeholder="筛选商户"
style={{ width: 200 }}
onChange={handleMerchantFilter}
allowClear
>
{merchants.map(merchant => (
<Option key={merchant.id} value={merchant.id}>
{merchant.companyName}
</Option>
))}
</Select>
<Select
placeholder="筛选平台"
style={{ width: 120 }}
onChange={handlePlatformFilter}
allowClear
>
<Option value="Amazon">Amazon</Option>
<Option value="eBay">eBay</Option>
<Option value="Shopee">Shopee</Option>
<Option value="TikTok">TikTok</Option>
</Select>
<Select
placeholder="筛选状态"
style={{ width: 120 }}
onChange={handleStatusFilter}
allowClear
>
<Option value="PENDING"></Option>
<Option value="PROCESSING"></Option>
<Option value="SHIPPED"></Option>
<Option value="DELIVERED"></Option>
<Option value="CANCELLED"></Option>
<Option value="REFUNDED">退</Option>
</Select>
<RangePicker
placeholder={['开始日期', '结束日期']}
onChange={handleDateRangeChange}
/>
</div>
<Table
columns={columns}
dataSource={filteredOrders}
rowKey="id"
loading={loading}
pagination={{
showSizeChanger: true,
pageSizeOptions: ['10', '20', '50'],
defaultPageSize: 10,
}}
/>
</Card>
);
};
export default MerchantOrderManage;

View File

@@ -0,0 +1,315 @@
import React, { useState, useEffect } from 'react';
import { Table, Input, Button, Select, DatePicker, message, Card, Typography, Tag, Modal, Form } from 'antd';
import { SearchOutlined, EyeOutlined, DownloadOutlined, ReloadOutlined } from '@ant-design/icons';
import { getMerchantSettlements, processSettlement } from '../../services/merchantSettlementService';
const { Option } = Select;
const { Title, Text } = Typography;
const { RangePicker } = DatePicker;
const { Item } = Form;
interface Settlement {
id: string;
merchantId: string;
merchantName: string;
periodStart: string;
periodEnd: string;
totalAmount: number;
status: 'PENDING' | 'PROCESSING' | 'COMPLETED' | 'FAILED';
createdAt: string;
updatedAt: string;
}
const MerchantSettlementManage: React.FC = () => {
const [settlements, setSettlements] = useState<Settlement[]>([]);
const [merchants, setMerchants] = useState<{ id: string; companyName: string }[]>([]);
const [loading, setLoading] = useState(false);
const [searchText, setSearchText] = useState('');
const [merchantFilter, setMerchantFilter] = useState<string>('');
const [statusFilter, setStatusFilter] = useState<string>('');
const [dateRange, setDateRange] = useState<[string, string] | null>(null);
const [isModalVisible, setIsModalVisible] = useState(false);
const [selectedSettlement, setSelectedSettlement] = useState<Settlement | null>(null);
const [form] = Form.useForm();
const fetchSettlements = async () => {
setLoading(true);
try {
const response = await getMerchantSettlements();
setSettlements(response.data);
} catch (error) {
message.error('获取结算列表失败');
console.error('获取结算列表失败:', error);
} finally {
setLoading(false);
}
};
const fetchMerchants = async () => {
try {
// 实际项目中应该调用获取商户列表的API
// 这里模拟数据
setMerchants([
{ id: '1', companyName: '商户A' },
{ id: '2', companyName: '商户B' },
{ id: '3', companyName: '商户C' },
]);
} catch (error) {
console.error('获取商户列表失败:', error);
}
};
useEffect(() => {
fetchMerchants();
fetchSettlements();
}, []);
const handleSearch = (value: string) => {
setSearchText(value);
};
const handleMerchantFilter = (value: string) => {
setMerchantFilter(value);
};
const handleStatusFilter = (value: string) => {
setStatusFilter(value);
};
const handleDateRangeChange = (dates: any) => {
if (dates) {
setDateRange([dates[0].format('YYYY-MM-DD'), dates[1].format('YYYY-MM-DD')]);
} else {
setDateRange(null);
}
};
const filteredSettlements = settlements.filter(settlement => {
const matchesSearch = settlement.id.includes(searchText) ||
settlement.merchantName.toLowerCase().includes(searchText.toLowerCase());
const matchesMerchant = merchantFilter ? settlement.merchantId === merchantFilter : true;
const matchesStatus = statusFilter ? settlement.status === statusFilter : true;
return matchesSearch && matchesMerchant && matchesStatus;
});
const handleView = (settlement: Settlement) => {
setSelectedSettlement(settlement);
setIsModalVisible(true);
};
const handleProcess = (settlement: Settlement) => {
// 调用API处理结算
message.info(`处理结算: ${settlement.id}`);
};
const handleDownload = (settlement: Settlement) => {
// 下载结算单
message.info(`下载结算单: ${settlement.id}`);
};
const handleRefresh = () => {
fetchSettlements();
};
const getStatusTag = (status: string) => {
const statusConfig: Record<string, { color: string; text: string }> = {
PENDING: { color: 'blue', text: '待处理' },
PROCESSING: { color: 'orange', text: '处理中' },
COMPLETED: { color: 'green', text: '已完成' },
FAILED: { color: 'red', text: '失败' },
};
const config = statusConfig[status] || { color: 'default', text: status };
return <Tag color={config.color}>{config.text}</Tag>;
};
const columns = [
{
title: '结算单ID',
dataIndex: 'id',
key: 'id',
ellipsis: true,
},
{
title: '商户',
dataIndex: 'merchantName',
key: 'merchantName',
ellipsis: true,
},
{
title: '结算周期',
key: 'period',
render: (_, settlement: Settlement) => (
<Text>
{settlement.periodStart} {settlement.periodEnd}
</Text>
),
},
{
title: '结算金额',
dataIndex: 'totalAmount',
key: 'totalAmount',
render: (amount: number) => <Text>${amount.toFixed(2)}</Text>,
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
render: (status: string) => getStatusTag(status),
},
{
title: '创建时间',
dataIndex: 'createdAt',
key: 'createdAt',
ellipsis: true,
},
{
title: '操作',
key: 'action',
render: (_: any, settlement: Settlement) => (
<div>
<Button
icon={<EyeOutlined />}
size="small"
style={{ marginRight: 8 }}
onClick={() => handleView(settlement)}
>
</Button>
<Button
icon={<DownloadOutlined />}
size="small"
style={{ marginRight: 8 }}
onClick={() => handleDownload(settlement)}
>
</Button>
{settlement.status === 'PENDING' && (
<Button
type="primary"
size="small"
onClick={() => handleProcess(settlement)}
>
</Button>
)}
</div>
),
},
];
return (
<Card>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 24 }}>
<Title level={4}></Title>
<Button
icon={<ReloadOutlined />}
onClick={handleRefresh}
>
</Button>
</div>
<div style={{ display: 'flex', marginBottom: 16, gap: 16, flexWrap: 'wrap' }}>
<Input
placeholder="搜索结算单ID或商户名称"
prefix={<SearchOutlined />}
style={{ width: 300 }}
onChange={(e) => handleSearch(e.target.value)}
/>
<Select
placeholder="筛选商户"
style={{ width: 200 }}
onChange={handleMerchantFilter}
allowClear
>
{merchants.map(merchant => (
<Option key={merchant.id} value={merchant.id}>
{merchant.companyName}
</Option>
))}
</Select>
<Select
placeholder="筛选状态"
style={{ width: 120 }}
onChange={handleStatusFilter}
allowClear
>
<Option value="PENDING"></Option>
<Option value="PROCESSING"></Option>
<Option value="COMPLETED"></Option>
<Option value="FAILED"></Option>
</Select>
<RangePicker
placeholder={['开始日期', '结束日期']}
onChange={handleDateRangeChange}
/>
</div>
<Table
columns={columns}
dataSource={filteredSettlements}
rowKey="id"
loading={loading}
pagination={{
showSizeChanger: true,
pageSizeOptions: ['10', '20', '50'],
defaultPageSize: 10,
}}
/>
<Modal
title="结算详情"
open={isModalVisible}
onCancel={() => setIsModalVisible(false)}
footer={[
<Button key="close" onClick={() => setIsModalVisible(false)}>
</Button>,
]}
width={600}
>
{selectedSettlement && (
<div>
<div style={{ marginBottom: 16 }}>
<Text strong>ID:</Text> {selectedSettlement.id}
</div>
<div style={{ marginBottom: 16 }}>
<Text strong>:</Text> {selectedSettlement.merchantName}
</div>
<div style={{ marginBottom: 16 }}>
<Text strong>:</Text> {selectedSettlement.periodStart} {selectedSettlement.periodEnd}
</div>
<div style={{ marginBottom: 16 }}>
<Text strong>:</Text> ${selectedSettlement.totalAmount.toFixed(2)}
</div>
<div style={{ marginBottom: 16 }}>
<Text strong>:</Text> {getStatusTag(selectedSettlement.status)}
</div>
<div style={{ marginBottom: 16 }}>
<Text strong>:</Text> {selectedSettlement.createdAt}
</div>
<div style={{ marginBottom: 16 }}>
<Text strong>:</Text> {selectedSettlement.updatedAt}
</div>
<div style={{ marginTop: 24 }}>
<Title level={5}></Title>
<Table
columns={[
{ title: '项目', dataIndex: 'item', key: 'item' },
{ title: '金额', dataIndex: 'amount', key: 'amount', render: (amount: number) => `$${amount.toFixed(2)}` },
]}
dataSource={[
{ key: '1', item: '商品销售', amount: selectedSettlement.totalAmount * 0.9 },
{ key: '2', item: '平台服务费', amount: selectedSettlement.totalAmount * 0.1 },
]}
pagination={false}
/>
</div>
</div>
)}
</Modal>
</Card>
);
};
export default MerchantSettlementManage;

View File

@@ -0,0 +1,358 @@
import React, { useState, useEffect } from 'react';
import { Table, Input, Button, Select, message, Card, Typography, Form, Modal } from 'antd';
import { SearchOutlined, PlusOutlined, EditOutlined, DeleteOutlined, EyeOutlined } from '@ant-design/icons';
import { getMerchantShops, createMerchantShop, updateMerchantShop, deleteMerchantShop } from '../../services/merchantShopService';
const { Option } = Select;
const { Title, Text } = Typography;
const { Item } = Form;
interface Shop {
id: string;
merchantId: string;
shopName: string;
platform: string;
shopUrl: string;
status: 'ACTIVE' | 'INACTIVE' | 'SUSPENDED';
createdAt: string;
updatedAt: string;
}
const MerchantShopManage: React.FC = () => {
const [shops, setShops] = useState<Shop[]>([]);
const [merchants, setMerchants] = useState<{ id: string; companyName: string }[]>([]);
const [loading, setLoading] = useState(false);
const [searchText, setSearchText] = useState('');
const [merchantFilter, setMerchantFilter] = useState<string>('');
const [platformFilter, setPlatformFilter] = useState<string>('');
const [statusFilter, setStatusFilter] = useState<string>('');
const [isModalVisible, setIsModalVisible] = useState(false);
const [form] = Form.useForm();
const [editingShop, setEditingShop] = useState<Shop | null>(null);
const fetchShops = async () => {
setLoading(true);
try {
const response = await getMerchantShops();
setShops(response.data);
} catch (error) {
message.error('获取店铺列表失败');
console.error('获取店铺列表失败:', error);
} finally {
setLoading(false);
}
};
const fetchMerchants = async () => {
try {
// 实际项目中应该调用获取商户列表的API
// 这里模拟数据
setMerchants([
{ id: '1', companyName: '商户A' },
{ id: '2', companyName: '商户B' },
{ id: '3', companyName: '商户C' },
]);
} catch (error) {
console.error('获取商户列表失败:', error);
}
};
useEffect(() => {
fetchMerchants();
fetchShops();
}, []);
const handleSearch = (value: string) => {
setSearchText(value);
};
const handleMerchantFilter = (value: string) => {
setMerchantFilter(value);
};
const handlePlatformFilter = (value: string) => {
setPlatformFilter(value);
};
const handleStatusFilter = (value: string) => {
setStatusFilter(value);
};
const filteredShops = shops.filter(shop => {
const matchesSearch = shop.shopName.toLowerCase().includes(searchText.toLowerCase()) ||
shop.shopUrl.toLowerCase().includes(searchText.toLowerCase());
const matchesMerchant = merchantFilter ? shop.merchantId === merchantFilter : true;
const matchesPlatform = platformFilter ? shop.platform === platformFilter : true;
const matchesStatus = statusFilter ? shop.status === statusFilter : true;
return matchesSearch && matchesMerchant && matchesPlatform && matchesStatus;
});
const handleCreate = () => {
setEditingShop(null);
form.resetFields();
setIsModalVisible(true);
};
const handleEdit = (shop: Shop) => {
setEditingShop(shop);
form.setFieldsValue(shop);
setIsModalVisible(true);
};
const handleDelete = (shopId: string) => {
// 确认删除后调用API
message.info(`删除店铺: ${shopId}`);
};
const handleView = (shop: Shop) => {
// 跳转到店铺详情页面
message.info(`查看店铺详情: ${shop.shopName}`);
};
const handleOk = async () => {
try {
const values = await form.validateFields();
if (editingShop) {
// 编辑店铺
await updateMerchantShop(editingShop.id, values);
message.success('店铺更新成功');
} else {
// 创建店铺
await createMerchantShop(values);
message.success('店铺创建成功');
}
setIsModalVisible(false);
fetchShops();
} catch (error) {
message.error('操作失败');
console.error('操作失败:', error);
}
};
const handleCancel = () => {
setIsModalVisible(false);
};
const columns = [
{
title: '店铺ID',
dataIndex: 'id',
key: 'id',
ellipsis: true,
},
{
title: '商户',
dataIndex: 'merchantId',
key: 'merchantId',
render: (merchantId: string) => {
const merchant = merchants.find(m => m.id === merchantId);
return <Text>{merchant?.companyName || merchantId}</Text>;
},
},
{
title: '店铺名称',
dataIndex: 'shopName',
key: 'shopName',
ellipsis: true,
},
{
title: '平台',
dataIndex: 'platform',
key: 'platform',
ellipsis: true,
},
{
title: '店铺链接',
dataIndex: 'shopUrl',
key: 'shopUrl',
ellipsis: true,
render: (shopUrl: string) => (
<a href={shopUrl} target="_blank" rel="noopener noreferrer">
{shopUrl}
</a>
),
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
render: (status: string) => {
const statusMap: Record<string, string> = {
ACTIVE: '活跃',
INACTIVE: '未激活',
SUSPENDED: '暂停',
};
return <Text>{statusMap[status] || status}</Text>;
},
},
{
title: '创建时间',
dataIndex: 'createdAt',
key: 'createdAt',
ellipsis: true,
},
{
title: '操作',
key: 'action',
render: (_: any, shop: Shop) => (
<div>
<Button
icon={<EyeOutlined />}
size="small"
style={{ marginRight: 8 }}
onClick={() => handleView(shop)}
>
</Button>
<Button
icon={<EditOutlined />}
size="small"
style={{ marginRight: 8 }}
onClick={() => handleEdit(shop)}
>
</Button>
<Button
icon={<DeleteOutlined />}
size="small"
danger
onClick={() => handleDelete(shop.id)}
>
</Button>
</div>
),
},
];
return (
<Card>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 24 }}>
<Title level={4}></Title>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={handleCreate}
>
</Button>
</div>
<div style={{ display: 'flex', marginBottom: 16, gap: 16, flexWrap: 'wrap' }}>
<Input
placeholder="搜索店铺名称或链接"
prefix={<SearchOutlined />}
style={{ width: 300 }}
onChange={(e) => handleSearch(e.target.value)}
/>
<Select
placeholder="筛选商户"
style={{ width: 200 }}
onChange={handleMerchantFilter}
allowClear
>
{merchants.map(merchant => (
<Option key={merchant.id} value={merchant.id}>
{merchant.companyName}
</Option>
))}
</Select>
<Select
placeholder="筛选平台"
style={{ width: 120 }}
onChange={handlePlatformFilter}
allowClear
>
<Option value="Amazon">Amazon</Option>
<Option value="eBay">eBay</Option>
<Option value="Shopee">Shopee</Option>
<Option value="TikTok">TikTok</Option>
</Select>
<Select
placeholder="筛选状态"
style={{ width: 120 }}
onChange={handleStatusFilter}
allowClear
>
<Option value="ACTIVE"></Option>
<Option value="INACTIVE"></Option>
<Option value="SUSPENDED"></Option>
</Select>
</div>
<Table
columns={columns}
dataSource={filteredShops}
rowKey="id"
loading={loading}
pagination={{
showSizeChanger: true,
pageSizeOptions: ['10', '20', '50'],
defaultPageSize: 10,
}}
/>
<Modal
title={editingShop ? '编辑店铺' : '新增店铺'}
open={isModalVisible}
onOk={handleOk}
onCancel={handleCancel}
>
<Form form={form} layout="vertical">
<Item
name="merchantId"
label="商户"
rules={[{ required: true, message: '请选择商户' }]}
>
<Select placeholder="选择商户">
{merchants.map(merchant => (
<Option key={merchant.id} value={merchant.id}>
{merchant.companyName}
</Option>
))}
</Select>
</Item>
<Item
name="shopName"
label="店铺名称"
rules={[{ required: true, message: '请输入店铺名称' }]}
>
<Input placeholder="请输入店铺名称" />
</Item>
<Item
name="platform"
label="平台"
rules={[{ required: true, message: '请选择平台' }]}
>
<Select placeholder="选择平台">
<Option value="Amazon">Amazon</Option>
<Option value="eBay">eBay</Option>
<Option value="Shopee">Shopee</Option>
<Option value="TikTok">TikTok</Option>
</Select>
</Item>
<Item
name="shopUrl"
label="店铺链接"
rules={[{ required: true, message: '请输入店铺链接' }]}
>
<Input placeholder="请输入店铺链接" />
</Item>
<Item
name="status"
label="状态"
rules={[{ required: true, message: '请选择状态' }]}
>
<Select placeholder="选择状态">
<Option value="ACTIVE"></Option>
<Option value="INACTIVE"></Option>
<Option value="SUSPENDED"></Option>
</Select>
</Item>
</Form>
</Modal>
</Card>
);
};
export default MerchantShopManage;

View File

@@ -0,0 +1,18 @@
import MerchantManage from './MerchantManage';
import MerchantShopManage from './MerchantShopManage';
import MerchantOrderManage from './MerchantOrderManage';
import MerchantSettlementManage from './MerchantSettlementManage';
export {
MerchantManage,
MerchantShopManage,
MerchantOrderManage,
MerchantSettlementManage,
};
export default {
MerchantManage,
MerchantShopManage,
MerchantOrderManage,
MerchantSettlementManage,
};

View File

@@ -0,0 +1,135 @@
// 商户订单管理服务
interface Order {
id: string;
merchantId: string;
merchantName: string;
orderId: string;
platform: string;
totalAmount: number;
status: 'PENDING' | 'PROCESSING' | 'SHIPPED' | 'DELIVERED' | 'CANCELLED' | 'REFUNDED';
createdAt: string;
updatedAt: string;
}
interface OrderResponse {
data: Order[];
total: number;
page: number;
pageSize: number;
}
// 模拟数据
const mockOrders: Order[] = [
{
id: '1',
merchantId: '1',
merchantName: '商户A',
orderId: 'AMZ123456',
platform: 'Amazon',
totalAmount: 100.50,
status: 'DELIVERED',
createdAt: '2026-03-01T10:00:00Z',
updatedAt: '2026-03-03T10:00:00Z',
},
{
id: '2',
merchantId: '1',
merchantName: '商户A',
orderId: 'EBAY789012',
platform: 'eBay',
totalAmount: 50.25,
status: 'SHIPPED',
createdAt: '2026-03-02T10:00:00Z',
updatedAt: '2026-03-03T10:00:00Z',
},
{
id: '3',
merchantId: '2',
merchantName: '商户B',
orderId: 'SHOPEE345678',
platform: 'Shopee',
totalAmount: 75.75,
status: 'PROCESSING',
createdAt: '2026-03-03T10:00:00Z',
updatedAt: '2026-03-03T10:00:00Z',
},
{
id: '4',
merchantId: '3',
merchantName: '商户C',
orderId: 'TIKTOK901234',
platform: 'TikTok',
totalAmount: 120.00,
status: 'PENDING',
createdAt: '2026-03-04T10:00:00Z',
updatedAt: '2026-03-04T10:00:00Z',
},
{
id: '5',
merchantId: '1',
merchantName: '商户A',
orderId: 'AMZ567890',
platform: 'Amazon',
totalAmount: 200.00,
status: 'CANCELLED',
createdAt: '2026-03-05T10:00:00Z',
updatedAt: '2026-03-05T10:00:00Z',
},
];
// 获取商户订单列表
export const getMerchantOrders = async (): Promise<OrderResponse> => {
// 模拟API请求延迟
await new Promise(resolve => setTimeout(resolve, 500));
return {
data: mockOrders,
total: mockOrders.length,
page: 1,
pageSize: 10,
};
};
// 根据商户ID获取订单列表
export const getOrdersByMerchantId = async (merchantId: string): Promise<Order[]> => {
// 模拟API请求延迟
await new Promise(resolve => setTimeout(resolve, 500));
return mockOrders.filter(o => o.merchantId === merchantId);
};
// 更新订单状态
export const updateOrderStatus = async (orderId: string, status: Order['status']): Promise<Order> => {
// 模拟API请求延迟
await new Promise(resolve => setTimeout(resolve, 500));
const index = mockOrders.findIndex(o => o.id === orderId);
if (index === -1) {
throw new Error('订单不存在');
}
mockOrders[index] = {
...mockOrders[index],
status,
updatedAt: new Date().toISOString(),
};
return mockOrders[index];
};
// 获取订单详情
export const getOrderById = async (orderId: string): Promise<Order> => {
// 模拟API请求延迟
await new Promise(resolve => setTimeout(resolve, 500));
const order = mockOrders.find(o => o.id === orderId);
if (!order) {
throw new Error('订单不存在');
}
return order;
};
// 搜索订单
export const searchOrders = async (keyword: string): Promise<Order[]> => {
// 模拟API请求延迟
await new Promise(resolve => setTimeout(resolve, 500));
return mockOrders.filter(o =>
o.orderId.includes(keyword) ||
o.merchantName.toLowerCase().includes(keyword.toLowerCase())
);
};

View File

@@ -0,0 +1,137 @@
// 商户管理服务
interface Merchant {
id: string;
tenantId: string;
companyName: string;
businessLicense: string;
contactEmail: string;
contactPhone: string;
status: 'PENDING' | 'ACTIVE' | 'SUSPENDED' | 'TERMINATED';
tier: 'BASIC' | 'PRO' | 'ENTERPRISE';
traceId: string;
createdAt: string;
updatedAt: string;
}
interface MerchantResponse {
data: Merchant[];
total: number;
page: number;
pageSize: number;
}
// 模拟数据
const mockMerchants: Merchant[] = [
{
id: '1',
tenantId: 'tenant1',
companyName: '商户A',
businessLicense: '1234567890',
contactEmail: 'merchantA@example.com',
contactPhone: '13800138001',
status: 'ACTIVE',
tier: 'PRO',
traceId: 'trace1',
createdAt: '2026-03-01T10:00:00Z',
updatedAt: '2026-03-01T10:00:00Z',
},
{
id: '2',
tenantId: 'tenant2',
companyName: '商户B',
businessLicense: '0987654321',
contactEmail: 'merchantB@example.com',
contactPhone: '13900139001',
status: 'PENDING',
tier: 'BASIC',
traceId: 'trace2',
createdAt: '2026-03-02T10:00:00Z',
updatedAt: '2026-03-02T10:00:00Z',
},
{
id: '3',
tenantId: 'tenant3',
companyName: '商户C',
businessLicense: '1122334455',
contactEmail: 'merchantC@example.com',
contactPhone: '13700137001',
status: 'SUSPENDED',
tier: 'ENTERPRISE',
traceId: 'trace3',
createdAt: '2026-03-03T10:00:00Z',
updatedAt: '2026-03-03T10:00:00Z',
},
];
// 获取商户列表
export const getMerchants = async (): Promise<MerchantResponse> => {
// 模拟API请求延迟
await new Promise(resolve => setTimeout(resolve, 500));
return {
data: mockMerchants,
total: mockMerchants.length,
page: 1,
pageSize: 10,
};
};
// 创建商户
export const createMerchant = async (merchantData: Partial<Merchant>): Promise<Merchant> => {
// 模拟API请求延迟
await new Promise(resolve => setTimeout(resolve, 500));
const newMerchant: Merchant = {
id: `new-${Date.now()}`,
tenantId: merchantData.tenantId || 'default-tenant',
companyName: merchantData.companyName || '',
businessLicense: merchantData.businessLicense || '',
contactEmail: merchantData.contactEmail || '',
contactPhone: merchantData.contactPhone || '',
status: merchantData.status || 'PENDING',
tier: merchantData.tier || 'BASIC',
traceId: `trace-${Date.now()}`,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
mockMerchants.push(newMerchant);
return newMerchant;
};
// 更新商户
export const updateMerchant = async (merchantId: string, merchantData: Partial<Merchant>): Promise<Merchant> => {
// 模拟API请求延迟
await new Promise(resolve => setTimeout(resolve, 500));
const index = mockMerchants.findIndex(m => m.id === merchantId);
if (index === -1) {
throw new Error('商户不存在');
}
mockMerchants[index] = {
...mockMerchants[index],
...merchantData,
updatedAt: new Date().toISOString(),
};
return mockMerchants[index];
};
// 删除商户
export const deleteMerchant = async (merchantId: string): Promise<boolean> => {
// 模拟API请求延迟
await new Promise(resolve => setTimeout(resolve, 500));
const index = mockMerchants.findIndex(m => m.id === merchantId);
if (index === -1) {
throw new Error('商户不存在');
}
mockMerchants.splice(index, 1);
return true;
};
// 获取商户详情
export const getMerchantById = async (merchantId: string): Promise<Merchant> => {
// 模拟API请求延迟
await new Promise(resolve => setTimeout(resolve, 500));
const merchant = mockMerchants.find(m => m.id === merchantId);
if (!merchant) {
throw new Error('商户不存在');
}
return merchant;
};

View File

@@ -0,0 +1,133 @@
// 商户结算管理服务
interface Settlement {
id: string;
merchantId: string;
merchantName: string;
periodStart: string;
periodEnd: string;
totalAmount: number;
status: 'PENDING' | 'PROCESSING' | 'COMPLETED' | 'FAILED';
createdAt: string;
updatedAt: string;
}
interface SettlementResponse {
data: Settlement[];
total: number;
page: number;
pageSize: number;
}
// 模拟数据
const mockSettlements: Settlement[] = [
{
id: '1',
merchantId: '1',
merchantName: '商户A',
periodStart: '2026-02-01',
periodEnd: '2026-02-29',
totalAmount: 15000.00,
status: 'COMPLETED',
createdAt: '2026-03-01T10:00:00Z',
updatedAt: '2026-03-02T10:00:00Z',
},
{
id: '2',
merchantId: '2',
merchantName: '商户B',
periodStart: '2026-02-01',
periodEnd: '2026-02-29',
totalAmount: 8000.00,
status: 'COMPLETED',
createdAt: '2026-03-01T10:00:00Z',
updatedAt: '2026-03-02T10:00:00Z',
},
{
id: '3',
merchantId: '3',
merchantName: '商户C',
periodStart: '2026-02-01',
periodEnd: '2026-02-29',
totalAmount: 12000.00,
status: 'PROCESSING',
createdAt: '2026-03-01T10:00:00Z',
updatedAt: '2026-03-01T10:00:00Z',
},
{
id: '4',
merchantId: '1',
merchantName: '商户A',
periodStart: '2026-03-01',
periodEnd: '2026-03-31',
totalAmount: 0.00,
status: 'PENDING',
createdAt: '2026-04-01T10:00:00Z',
updatedAt: '2026-04-01T10:00:00Z',
},
];
// 获取商户结算列表
export const getMerchantSettlements = async (): Promise<SettlementResponse> => {
// 模拟API请求延迟
await new Promise(resolve => setTimeout(resolve, 500));
return {
data: mockSettlements,
total: mockSettlements.length,
page: 1,
pageSize: 10,
};
};
// 根据商户ID获取结算列表
export const getSettlementsByMerchantId = async (merchantId: string): Promise<Settlement[]> => {
// 模拟API请求延迟
await new Promise(resolve => setTimeout(resolve, 500));
return mockSettlements.filter(s => s.merchantId === merchantId);
};
// 处理结算
export const processSettlement = async (settlementId: string): Promise<Settlement> => {
// 模拟API请求延迟
await new Promise(resolve => setTimeout(resolve, 1000));
const index = mockSettlements.findIndex(s => s.id === settlementId);
if (index === -1) {
throw new Error('结算单不存在');
}
mockSettlements[index] = {
...mockSettlements[index],
status: 'COMPLETED',
updatedAt: new Date().toISOString(),
};
return mockSettlements[index];
};
// 获取结算详情
export const getSettlementById = async (settlementId: string): Promise<Settlement> => {
// 模拟API请求延迟
await new Promise(resolve => setTimeout(resolve, 500));
const settlement = mockSettlements.find(s => s.id === settlementId);
if (!settlement) {
throw new Error('结算单不存在');
}
return settlement;
};
// 创建结算单
export const createSettlement = async (settlementData: Partial<Settlement>): Promise<Settlement> => {
// 模拟API请求延迟
await new Promise(resolve => setTimeout(resolve, 500));
const newSettlement: Settlement = {
id: `new-${Date.now()}`,
merchantId: settlementData.merchantId || '',
merchantName: settlementData.merchantName || '',
periodStart: settlementData.periodStart || '',
periodEnd: settlementData.periodEnd || '',
totalAmount: settlementData.totalAmount || 0,
status: 'PENDING',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
mockSettlements.push(newSettlement);
return newSettlement;
};

View File

@@ -0,0 +1,139 @@
// 商户店铺管理服务
interface Shop {
id: string;
merchantId: string;
shopName: string;
platform: string;
shopUrl: string;
status: 'ACTIVE' | 'INACTIVE' | 'SUSPENDED';
createdAt: string;
updatedAt: string;
}
interface ShopResponse {
data: Shop[];
total: number;
page: number;
pageSize: number;
}
// 模拟数据
const mockShops: Shop[] = [
{
id: '1',
merchantId: '1',
shopName: '商户A的Amazon店铺',
platform: 'Amazon',
shopUrl: 'https://www.amazon.com/shop/merchantA',
status: 'ACTIVE',
createdAt: '2026-03-01T10:00:00Z',
updatedAt: '2026-03-01T10:00:00Z',
},
{
id: '2',
merchantId: '1',
shopName: '商户A的eBay店铺',
platform: 'eBay',
shopUrl: 'https://www.ebay.com/str/merchantA',
status: 'ACTIVE',
createdAt: '2026-03-02T10:00:00Z',
updatedAt: '2026-03-02T10:00:00Z',
},
{
id: '3',
merchantId: '2',
shopName: '商户B的Shopee店铺',
platform: 'Shopee',
shopUrl: 'https://shopee.com.my/merchantB',
status: 'INACTIVE',
createdAt: '2026-03-03T10:00:00Z',
updatedAt: '2026-03-03T10:00:00Z',
},
{
id: '4',
merchantId: '3',
shopName: '商户C的TikTok店铺',
platform: 'TikTok',
shopUrl: 'https://www.tiktok.com/@merchantC',
status: 'SUSPENDED',
createdAt: '2026-03-04T10:00:00Z',
updatedAt: '2026-03-04T10:00:00Z',
},
];
// 获取商户店铺列表
export const getMerchantShops = async (): Promise<ShopResponse> => {
// 模拟API请求延迟
await new Promise(resolve => setTimeout(resolve, 500));
return {
data: mockShops,
total: mockShops.length,
page: 1,
pageSize: 10,
};
};
// 创建商户店铺
export const createMerchantShop = async (shopData: Partial<Shop>): Promise<Shop> => {
// 模拟API请求延迟
await new Promise(resolve => setTimeout(resolve, 500));
const newShop: Shop = {
id: `new-${Date.now()}`,
merchantId: shopData.merchantId || '',
shopName: shopData.shopName || '',
platform: shopData.platform || '',
shopUrl: shopData.shopUrl || '',
status: shopData.status || 'INACTIVE',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
mockShops.push(newShop);
return newShop;
};
// 更新商户店铺
export const updateMerchantShop = async (shopId: string, shopData: Partial<Shop>): Promise<Shop> => {
// 模拟API请求延迟
await new Promise(resolve => setTimeout(resolve, 500));
const index = mockShops.findIndex(s => s.id === shopId);
if (index === -1) {
throw new Error('店铺不存在');
}
mockShops[index] = {
...mockShops[index],
...shopData,
updatedAt: new Date().toISOString(),
};
return mockShops[index];
};
// 删除商户店铺
export const deleteMerchantShop = async (shopId: string): Promise<boolean> => {
// 模拟API请求延迟
await new Promise(resolve => setTimeout(resolve, 500));
const index = mockShops.findIndex(s => s.id === shopId);
if (index === -1) {
throw new Error('店铺不存在');
}
mockShops.splice(index, 1);
return true;
};
// 获取店铺详情
export const getShopById = async (shopId: string): Promise<Shop> => {
// 模拟API请求延迟
await new Promise(resolve => setTimeout(resolve, 500));
const shop = mockShops.find(s => s.id === shopId);
if (!shop) {
throw new Error('店铺不存在');
}
return shop;
};
// 根据商户ID获取店铺列表
export const getShopsByMerchantId = async (merchantId: string): Promise<Shop[]> => {
// 模拟API请求延迟
await new Promise(resolve => setTimeout(resolve, 500));
return mockShops.filter(s => s.merchantId === merchantId);
};