395 lines
13 KiB
TypeScript
395 lines
13 KiB
TypeScript
|
|
/**
|
|||
|
|
* [MOCK] 多店铺报表数据源
|
|||
|
|
* AI注意: 这是Mock实现,不是真实业务逻辑
|
|||
|
|
* 仅在USE_MOCK=true时启用
|
|||
|
|
*/
|
|||
|
|
|
|||
|
|
export type ReportType = 'SALES' | 'PROFIT' | 'INVENTORY' | 'ORDER' | 'AD' | 'REFUND';
|
|||
|
|
export type TimeDimension = 'DAILY' | 'WEEKLY' | 'MONTHLY' | 'QUARTERLY' | 'YEARLY';
|
|||
|
|
|
|||
|
|
export interface ShopInfo {
|
|||
|
|
id: string;
|
|||
|
|
tenant_id: string;
|
|||
|
|
name: string;
|
|||
|
|
platform: string;
|
|||
|
|
status: 'ACTIVE' | 'INACTIVE' | 'SUSPENDED';
|
|||
|
|
region: string;
|
|||
|
|
currency: string;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export interface AggregatedReport {
|
|||
|
|
id: string;
|
|||
|
|
tenant_id: string;
|
|||
|
|
report_type: ReportType;
|
|||
|
|
time_dimension: TimeDimension;
|
|||
|
|
start_date: string;
|
|||
|
|
end_date: string;
|
|||
|
|
summary: {
|
|||
|
|
total_shops: number;
|
|||
|
|
active_shops: number;
|
|||
|
|
total_revenue: number;
|
|||
|
|
total_cost: number;
|
|||
|
|
total_profit: number;
|
|||
|
|
total_orders: number;
|
|||
|
|
total_quantity: number;
|
|||
|
|
avg_order_value: number;
|
|||
|
|
profit_margin: number;
|
|||
|
|
};
|
|||
|
|
shop_breakdown: Array<{
|
|||
|
|
shop_id: string;
|
|||
|
|
shop_name: string;
|
|||
|
|
platform: string;
|
|||
|
|
revenue: number;
|
|||
|
|
cost: number;
|
|||
|
|
profit: number;
|
|||
|
|
orders: number;
|
|||
|
|
quantity: number;
|
|||
|
|
avg_order_value: number;
|
|||
|
|
profit_margin: number;
|
|||
|
|
contribution_rate: number;
|
|||
|
|
}>;
|
|||
|
|
time_series: Array<{
|
|||
|
|
date: string;
|
|||
|
|
revenue: number;
|
|||
|
|
profit: number;
|
|||
|
|
orders: number;
|
|||
|
|
shop_count: number;
|
|||
|
|
}>;
|
|||
|
|
platform_breakdown: Array<{
|
|||
|
|
platform: string;
|
|||
|
|
revenue: number;
|
|||
|
|
profit: number;
|
|||
|
|
orders: number;
|
|||
|
|
shop_count: number;
|
|||
|
|
}>;
|
|||
|
|
rankings: {
|
|||
|
|
top_revenue_shops: Array<{ shop_id: string; shop_name: string; value: number }>;
|
|||
|
|
top_profit_shops: Array<{ shop_id: string; shop_name: string; value: number }>;
|
|||
|
|
top_growth_shops: Array<{ shop_id: string; shop_name: string; growth_rate: number }>;
|
|||
|
|
};
|
|||
|
|
comparison: {
|
|||
|
|
revenue_growth: number;
|
|||
|
|
profit_growth: number;
|
|||
|
|
order_growth: number;
|
|||
|
|
shop_growth: number;
|
|||
|
|
};
|
|||
|
|
created_at: string;
|
|||
|
|
updated_at: string;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export interface ReportSubscription {
|
|||
|
|
id: string;
|
|||
|
|
tenant_id: string;
|
|||
|
|
user_id: string;
|
|||
|
|
report_type: ReportType;
|
|||
|
|
time_dimension: TimeDimension;
|
|||
|
|
frequency: 'DAILY' | 'WEEKLY' | 'MONTHLY';
|
|||
|
|
recipients: string[];
|
|||
|
|
enabled: boolean;
|
|||
|
|
last_sent_at?: string;
|
|||
|
|
next_send_at?: string;
|
|||
|
|
created_at: string;
|
|||
|
|
updated_at: string;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export interface IShopReportDataSource {
|
|||
|
|
generateReport(params: {
|
|||
|
|
tenant_id: string;
|
|||
|
|
report_type: ReportType;
|
|||
|
|
time_dimension: TimeDimension;
|
|||
|
|
start_date: string;
|
|||
|
|
end_date: string;
|
|||
|
|
shop_ids?: string[];
|
|||
|
|
platforms?: string[];
|
|||
|
|
}): Promise<AggregatedReport>;
|
|||
|
|
fetchHistoricalReports(tenantId: string, reportType?: ReportType, limit?: number): Promise<AggregatedReport[]>;
|
|||
|
|
fetchReportById(reportId: string): Promise<AggregatedReport | null>;
|
|||
|
|
createSubscription(params: {
|
|||
|
|
tenant_id: string;
|
|||
|
|
report_type: ReportType;
|
|||
|
|
time_dimension: TimeDimension;
|
|||
|
|
frequency: 'DAILY' | 'WEEKLY' | 'MONTHLY';
|
|||
|
|
recipients: string[];
|
|||
|
|
}): Promise<ReportSubscription>;
|
|||
|
|
fetchUserSubscriptions(tenantId: string): Promise<ReportSubscription[]>;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const REPORT_TYPE_NAMES: Record<ReportType, string> = {
|
|||
|
|
SALES: '销售报表',
|
|||
|
|
PROFIT: '利润报表',
|
|||
|
|
INVENTORY: '库存报表',
|
|||
|
|
ORDER: '订单报表',
|
|||
|
|
AD: '广告报表',
|
|||
|
|
REFUND: '退款报表',
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const PLATFORM_NAMES: Record<string, string> = {
|
|||
|
|
AMAZON: '亚马逊',
|
|||
|
|
EBAY: 'eBay',
|
|||
|
|
SHOPEE: 'Shopee',
|
|||
|
|
TIKTOK: 'TikTok',
|
|||
|
|
TEMU: 'Temu',
|
|||
|
|
LAZADA: 'Lazada',
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
class MockShopReportDataSource implements IShopReportDataSource {
|
|||
|
|
private reports: AggregatedReport[] = [];
|
|||
|
|
private subscriptions: ReportSubscription[] = [];
|
|||
|
|
|
|||
|
|
constructor() {
|
|||
|
|
this.initMockData();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private initMockData() {
|
|||
|
|
const shops = [
|
|||
|
|
{ id: 'shop-001', name: '店铺A', platform: 'AMAZON' },
|
|||
|
|
{ id: 'shop-002', name: '店铺B', platform: 'EBAY' },
|
|||
|
|
{ id: 'shop-003', name: '店铺C', platform: 'SHOPEE' },
|
|||
|
|
{ id: 'shop-004', name: '店铺D', platform: 'AMAZON' },
|
|||
|
|
{ id: 'shop-005', name: '店铺E', platform: 'TIKTOK' },
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
// 生成模拟报表
|
|||
|
|
const reportTypes: ReportType[] = ['SALES', 'PROFIT', 'ORDER'];
|
|||
|
|
const timeDimensions: TimeDimension[] = ['DAILY', 'WEEKLY', 'MONTHLY'];
|
|||
|
|
|
|||
|
|
for (let i = 0; i < 5; i++) {
|
|||
|
|
const reportType = reportTypes[i % reportTypes.length];
|
|||
|
|
const timeDimension = timeDimensions[i % timeDimensions.length];
|
|||
|
|
const totalRevenue = 50000 + Math.random() * 100000;
|
|||
|
|
const totalProfit = totalRevenue * (0.15 + Math.random() * 0.15);
|
|||
|
|
|
|||
|
|
const shopBreakdown = shops.map((shop, index) => {
|
|||
|
|
const shopRevenue = totalRevenue / shops.length * (0.8 + Math.random() * 0.4);
|
|||
|
|
const shopProfit = shopRevenue * (0.1 + Math.random() * 0.2);
|
|||
|
|
return {
|
|||
|
|
shop_id: shop.id,
|
|||
|
|
shop_name: shop.name,
|
|||
|
|
platform: shop.platform,
|
|||
|
|
revenue: shopRevenue,
|
|||
|
|
cost: shopRevenue - shopProfit,
|
|||
|
|
profit: shopProfit,
|
|||
|
|
orders: Math.floor(100 + Math.random() * 500),
|
|||
|
|
quantity: Math.floor(500 + Math.random() * 2000),
|
|||
|
|
avg_order_value: shopRevenue / (100 + Math.random() * 500),
|
|||
|
|
profit_margin: (shopProfit / shopRevenue) * 100,
|
|||
|
|
contribution_rate: (shopRevenue / totalRevenue) * 100,
|
|||
|
|
};
|
|||
|
|
}).sort((a, b) => b.revenue - a.revenue);
|
|||
|
|
|
|||
|
|
const timeSeries = Array.from({ length: 7 }, (_, day) => ({
|
|||
|
|
date: new Date(Date.now() - (6 - day) * 86400000).toISOString().split('T')[0],
|
|||
|
|
revenue: totalRevenue / 7 * (0.8 + Math.random() * 0.4),
|
|||
|
|
profit: totalProfit / 7 * (0.8 + Math.random() * 0.4),
|
|||
|
|
orders: Math.floor((100 + Math.random() * 500) / 7),
|
|||
|
|
shop_count: shops.length,
|
|||
|
|
}));
|
|||
|
|
|
|||
|
|
const platformBreakdown = Object.entries(
|
|||
|
|
shops.reduce((acc, shop) => {
|
|||
|
|
if (!acc[shop.platform]) {
|
|||
|
|
acc[shop.platform] = { revenue: 0, profit: 0, orders: 0, shop_count: 0 };
|
|||
|
|
}
|
|||
|
|
const shopData = shopBreakdown.find(s => s.shop_id === shop.id);
|
|||
|
|
if (shopData) {
|
|||
|
|
acc[shop.platform].revenue += shopData.revenue;
|
|||
|
|
acc[shop.platform].profit += shopData.profit;
|
|||
|
|
acc[shop.platform].orders += shopData.orders;
|
|||
|
|
acc[shop.platform].shop_count += 1;
|
|||
|
|
}
|
|||
|
|
return acc;
|
|||
|
|
}, {} as Record<string, any>)
|
|||
|
|
).map(([platform, data]) => ({ platform, ...data }));
|
|||
|
|
|
|||
|
|
this.reports.push({
|
|||
|
|
id: `RPT-${i + 1}`,
|
|||
|
|
tenant_id: 'tenant-001',
|
|||
|
|
report_type: reportType,
|
|||
|
|
time_dimension: timeDimension,
|
|||
|
|
start_date: new Date(Date.now() - 7 * 86400000).toISOString().split('T')[0],
|
|||
|
|
end_date: new Date().toISOString().split('T')[0],
|
|||
|
|
summary: {
|
|||
|
|
total_shops: shops.length,
|
|||
|
|
active_shops: shops.length,
|
|||
|
|
total_revenue: totalRevenue,
|
|||
|
|
total_cost: totalRevenue - totalProfit,
|
|||
|
|
total_profit: totalProfit,
|
|||
|
|
total_orders: shopBreakdown.reduce((sum, s) => sum + s.orders, 0),
|
|||
|
|
total_quantity: shopBreakdown.reduce((sum, s) => sum + s.quantity, 0),
|
|||
|
|
avg_order_value: totalRevenue / shopBreakdown.reduce((sum, s) => sum + s.orders, 0),
|
|||
|
|
profit_margin: (totalProfit / totalRevenue) * 100,
|
|||
|
|
},
|
|||
|
|
shop_breakdown: shopBreakdown,
|
|||
|
|
time_series: timeSeries,
|
|||
|
|
platform_breakdown: platformBreakdown,
|
|||
|
|
rankings: {
|
|||
|
|
top_revenue_shops: shopBreakdown.slice(0, 3).map(s => ({
|
|||
|
|
shop_id: s.shop_id,
|
|||
|
|
shop_name: s.shop_name,
|
|||
|
|
value: s.revenue,
|
|||
|
|
})),
|
|||
|
|
top_profit_shops: shopBreakdown.slice(0, 3).map(s => ({
|
|||
|
|
shop_id: s.shop_id,
|
|||
|
|
shop_name: s.shop_name,
|
|||
|
|
value: s.profit,
|
|||
|
|
})),
|
|||
|
|
top_growth_shops: shopBreakdown.slice(0, 3).map((s, i) => ({
|
|||
|
|
shop_id: s.shop_id,
|
|||
|
|
shop_name: s.shop_name,
|
|||
|
|
growth_rate: 15 + Math.random() * 20 - i * 5,
|
|||
|
|
})),
|
|||
|
|
},
|
|||
|
|
comparison: {
|
|||
|
|
revenue_growth: 12.5 + Math.random() * 10,
|
|||
|
|
profit_growth: 8.3 + Math.random() * 8,
|
|||
|
|
order_growth: 5.7 + Math.random() * 5,
|
|||
|
|
shop_growth: 2.1 + Math.random() * 3,
|
|||
|
|
},
|
|||
|
|
created_at: new Date(Date.now() - i * 86400000).toISOString(),
|
|||
|
|
updated_at: new Date(Date.now() - i * 86400000).toISOString(),
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async generateReport(params: {
|
|||
|
|
tenant_id: string;
|
|||
|
|
report_type: ReportType;
|
|||
|
|
time_dimension: TimeDimension;
|
|||
|
|
start_date: string;
|
|||
|
|
end_date: string;
|
|||
|
|
shop_ids?: string[];
|
|||
|
|
platforms?: string[];
|
|||
|
|
}): Promise<AggregatedReport> {
|
|||
|
|
await this.delay(1000);
|
|||
|
|
const report = this.reports[0];
|
|||
|
|
return {
|
|||
|
|
...report,
|
|||
|
|
id: `RPT-${Date.now()}`,
|
|||
|
|
report_type: params.report_type,
|
|||
|
|
time_dimension: params.time_dimension,
|
|||
|
|
start_date: params.start_date,
|
|||
|
|
end_date: params.end_date,
|
|||
|
|
created_at: new Date().toISOString(),
|
|||
|
|
updated_at: new Date().toISOString(),
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async fetchHistoricalReports(tenantId: string, reportType?: ReportType, limit: number = 10): Promise<AggregatedReport[]> {
|
|||
|
|
await this.delay(300);
|
|||
|
|
let reports = this.reports.filter(r => r.tenant_id === tenantId);
|
|||
|
|
if (reportType) {
|
|||
|
|
reports = reports.filter(r => r.report_type === reportType);
|
|||
|
|
}
|
|||
|
|
return reports.slice(0, limit);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async fetchReportById(reportId: string): Promise<AggregatedReport | null> {
|
|||
|
|
await this.delay(200);
|
|||
|
|
return this.reports.find(r => r.id === reportId) || null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async createSubscription(params: {
|
|||
|
|
tenant_id: string;
|
|||
|
|
report_type: ReportType;
|
|||
|
|
time_dimension: TimeDimension;
|
|||
|
|
frequency: 'DAILY' | 'WEEKLY' | 'MONTHLY';
|
|||
|
|
recipients: string[];
|
|||
|
|
}): Promise<ReportSubscription> {
|
|||
|
|
await this.delay(300);
|
|||
|
|
const subscription: ReportSubscription = {
|
|||
|
|
id: `SUB-${Date.now()}`,
|
|||
|
|
tenant_id: params.tenant_id,
|
|||
|
|
user_id: 'user-001',
|
|||
|
|
report_type: params.report_type,
|
|||
|
|
time_dimension: params.time_dimension,
|
|||
|
|
frequency: params.frequency,
|
|||
|
|
recipients: params.recipients,
|
|||
|
|
enabled: true,
|
|||
|
|
created_at: new Date().toISOString(),
|
|||
|
|
updated_at: new Date().toISOString(),
|
|||
|
|
};
|
|||
|
|
this.subscriptions.push(subscription);
|
|||
|
|
return subscription;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async fetchUserSubscriptions(tenantId: string): Promise<ReportSubscription[]> {
|
|||
|
|
await this.delay(200);
|
|||
|
|
return this.subscriptions.filter(s => s.tenant_id === tenantId);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private delay(ms: number): Promise<void> {
|
|||
|
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
class ApiShopReportDataSource implements IShopReportDataSource {
|
|||
|
|
private baseUrl = '/api/shop-reports';
|
|||
|
|
|
|||
|
|
async generateReport(params: {
|
|||
|
|
tenant_id: string;
|
|||
|
|
report_type: ReportType;
|
|||
|
|
time_dimension: TimeDimension;
|
|||
|
|
start_date: string;
|
|||
|
|
end_date: string;
|
|||
|
|
shop_ids?: string[];
|
|||
|
|
platforms?: string[];
|
|||
|
|
}): Promise<AggregatedReport> {
|
|||
|
|
const response = await fetch(`${this.baseUrl}/generate`, {
|
|||
|
|
method: 'POST',
|
|||
|
|
headers: { 'Content-Type': 'application/json' },
|
|||
|
|
body: JSON.stringify(params),
|
|||
|
|
});
|
|||
|
|
if (!response.ok) throw new Error('Failed to generate report');
|
|||
|
|
const data = await response.json();
|
|||
|
|
return data.data;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async fetchHistoricalReports(tenantId: string, reportType?: ReportType, limit?: number): Promise<AggregatedReport[]> {
|
|||
|
|
const params = new URLSearchParams({ tenant_id: tenantId });
|
|||
|
|
if (reportType) params.append('report_type', reportType);
|
|||
|
|
if (limit) params.append('limit', limit.toString());
|
|||
|
|
const response = await fetch(`${this.baseUrl}/history?${params}`);
|
|||
|
|
if (!response.ok) throw new Error('Failed to fetch historical reports');
|
|||
|
|
const data = await response.json();
|
|||
|
|
return data.data;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async fetchReportById(reportId: string): Promise<AggregatedReport | null> {
|
|||
|
|
const response = await fetch(`${this.baseUrl}/${reportId}`);
|
|||
|
|
if (!response.ok) throw new Error('Failed to fetch report');
|
|||
|
|
const data = await response.json();
|
|||
|
|
return data.data;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async createSubscription(params: {
|
|||
|
|
tenant_id: string;
|
|||
|
|
report_type: ReportType;
|
|||
|
|
time_dimension: TimeDimension;
|
|||
|
|
frequency: 'DAILY' | 'WEEKLY' | 'MONTHLY';
|
|||
|
|
recipients: string[];
|
|||
|
|
}): Promise<ReportSubscription> {
|
|||
|
|
const response = await fetch(`${this.baseUrl}/subscriptions`, {
|
|||
|
|
method: 'POST',
|
|||
|
|
headers: { 'Content-Type': 'application/json' },
|
|||
|
|
body: JSON.stringify(params),
|
|||
|
|
});
|
|||
|
|
if (!response.ok) throw new Error('Failed to create subscription');
|
|||
|
|
const data = await response.json();
|
|||
|
|
return data.data;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async fetchUserSubscriptions(tenantId: string): Promise<ReportSubscription[]> {
|
|||
|
|
const response = await fetch(`${this.baseUrl}/subscriptions/my?tenant_id=${tenantId}`);
|
|||
|
|
if (!response.ok) throw new Error('Failed to fetch subscriptions');
|
|||
|
|
const data = await response.json();
|
|||
|
|
return data.data;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const useMock = process.env.REACT_APP_USE_MOCK === 'true';
|
|||
|
|
export const shopReportDataSource: IShopReportDataSource = useMock
|
|||
|
|
? new MockShopReportDataSource()
|
|||
|
|
: new ApiShopReportDataSource();
|
|||
|
|
|
|||
|
|
export { REPORT_TYPE_NAMES, PLATFORM_NAMES };
|