Files
makemd/dashboard/src/services/shopReportDataSource.ts
wurenzhi aa2cf560c6 feat: 添加汇率服务和缓存服务,优化数据源和日志服务
refactor: 重构数据源工厂和类型定义,提升代码可维护性

fix: 修复类型转换和状态机文档中的错误

docs: 更新服务架构文档,添加新的服务闭环流程

test: 添加汇率服务单元测试

chore: 清理无用代码和注释,优化代码结构
2026-03-19 14:19:01 +08:00

395 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* [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 };