feat: 添加汇率服务和缓存服务,优化数据源和日志服务

refactor: 重构数据源工厂和类型定义,提升代码可维护性

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

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

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

chore: 清理无用代码和注释,优化代码结构
This commit is contained in:
2026-03-19 14:19:01 +08:00
parent 0dac26d781
commit aa2cf560c6
120 changed files with 33383 additions and 4347 deletions

View File

@@ -0,0 +1,553 @@
/**
* [MOCK-ARBITRAGE] 跨平台套利数据源抽象层
* 通过环境变量自动切换Mock/真实API
* AI注意: 这是唯一入口,业务代码必须调用此层
*
* @module services/arbitrageDataSource
* @author AI-Agent-1
* @created 2026-03-19
*/
export interface ProfitSnapshot {
netProfit: number;
profitRate: number;
roi: number;
platformFee: number;
logisticsCost: number;
taxCost: number;
exchangeRate: number;
}
export interface ArbitrageOpportunity {
id: string;
tenant_id: string;
product_id: string;
product_name: string;
source_platform: string;
source_url: string;
source_price: number;
source_currency: string;
target_platform: string;
target_price: number;
target_currency: string;
profit_snapshot: ProfitSnapshot;
risk_level: 'LOW' | 'MEDIUM' | 'HIGH' | 'BLOCK';
opportunity_score: number;
status: 'PENDING' | 'APPROVED' | 'REJECTED' | 'EXECUTING' | 'EXECUTED' | 'FAILED';
execution_mode: 'AUTO' | 'MANUAL';
created_at: string;
updated_at: string;
executed_at?: string;
error_message?: string;
}
export interface ArbitrageExecution {
id: string;
opportunity_id: string;
tenant_id: string;
status: 'STARTED' | 'PURCHASED' | 'LISTED' | 'SOLD' | 'COMPLETED' | 'FAILED';
purchase_order_id?: string;
listing_id?: string;
sale_order_id?: string;
actual_purchase_price?: number;
actual_sale_price?: number;
actual_profit?: number;
notes?: string;
created_at: string;
updated_at: string;
}
export interface ArbitrageStats {
totalOpportunities: number;
pendingOpportunities: number;
executedOpportunities: number;
successRate: number;
totalProfit: number;
avgProfitMargin: number;
avgROI: number;
byPlatform: Record<string, { count: number; profit: number }>;
byStatus: Record<string, number>;
}
export interface OpportunityQueryParams {
status?: string;
platform?: string;
minScore?: number;
limit?: number;
offset?: number;
}
export interface CreateOpportunityParams {
productId: string;
productName: string;
sourcePlatform: string;
sourceUrl: string;
sourcePrice: number;
sourceCurrency: string;
targetPlatform: string;
targetPrice: number;
targetCurrency: string;
logisticsCost?: number;
adBudget?: number;
isB2B?: boolean;
executionMode?: 'AUTO' | 'MANUAL';
}
export interface ScanParams {
productIds: string;
sourcePlatform: string;
}
export interface IArbitrageDataSource {
getOpportunities(params?: OpportunityQueryParams): Promise<{ opportunities: ArbitrageOpportunity[]; total: number }>;
getOpportunityById(id: string): Promise<ArbitrageOpportunity | null>;
createOpportunity(params: CreateOpportunityParams): Promise<ArbitrageOpportunity>;
approveOpportunity(id: string): Promise<ArbitrageOpportunity>;
rejectOpportunity(id: string, reason: string): Promise<ArbitrageOpportunity>;
executeOpportunity(id: string): Promise<ArbitrageExecution>;
scanProducts(params: ScanParams): Promise<ArbitrageOpportunity[]>;
getStats(): Promise<ArbitrageStats>;
getExecutions(opportunityId: string): Promise<ArbitrageExecution[]>;
}
const USE_MOCK = process.env.REACT_APP_USE_MOCK === 'true';
const mockOpportunities: ArbitrageOpportunity[] = [
{
id: 'arb_001',
tenant_id: 'tenant_001',
product_id: 'prod_001',
product_name: 'Wireless Bluetooth Headphones Pro',
source_platform: '1688',
source_url: 'https://1688.com/item/12345',
source_price: 35,
source_currency: 'CNY',
target_platform: 'Amazon',
target_price: 29.99,
target_currency: 'USD',
profit_snapshot: {
netProfit: 12.5,
profitRate: 0.42,
roi: 0.85,
platformFee: 4.5,
logisticsCost: 3.0,
taxCost: 1.2,
exchangeRate: 0.14
},
risk_level: 'LOW',
opportunity_score: 85,
status: 'PENDING',
execution_mode: 'MANUAL',
created_at: '2026-03-19T10:00:00Z',
updated_at: '2026-03-19T10:00:00Z'
},
{
id: 'arb_002',
tenant_id: 'tenant_001',
product_id: 'prod_002',
product_name: 'USB-C Fast Charging Cable 3-Pack',
source_platform: 'Taobao',
source_url: 'https://taobao.com/item/67890',
source_price: 15,
source_currency: 'CNY',
target_platform: 'Temu',
target_price: 8.99,
target_currency: 'USD',
profit_snapshot: {
netProfit: 4.2,
profitRate: 0.35,
roi: 0.65,
platformFee: 1.5,
logisticsCost: 1.0,
taxCost: 0.5,
exchangeRate: 0.14
},
risk_level: 'MEDIUM',
opportunity_score: 72,
status: 'APPROVED',
execution_mode: 'MANUAL',
created_at: '2026-03-19T09:30:00Z',
updated_at: '2026-03-19T11:00:00Z'
},
{
id: 'arb_003',
tenant_id: 'tenant_001',
product_id: 'prod_003',
product_name: 'Smart Watch Series X',
source_platform: 'Alibaba',
source_url: 'https://alibaba.com/item/11111',
source_price: 120,
source_currency: 'CNY',
target_platform: 'Amazon',
target_price: 49.99,
target_currency: 'USD',
profit_snapshot: {
netProfit: 18.5,
profitRate: 0.37,
roi: 0.58,
platformFee: 7.5,
logisticsCost: 5.0,
taxCost: 2.0,
exchangeRate: 0.14
},
risk_level: 'LOW',
opportunity_score: 78,
status: 'EXECUTED',
execution_mode: 'AUTO',
created_at: '2026-03-18T14:00:00Z',
updated_at: '2026-03-18T18:00:00Z',
executed_at: '2026-03-18T18:00:00Z'
},
{
id: 'arb_004',
tenant_id: 'tenant_001',
product_id: 'prod_004',
product_name: 'LED Desk Lamp with Wireless Charger',
source_platform: '1688',
source_url: 'https://1688.com/item/22222',
source_price: 45,
source_currency: 'CNY',
target_platform: 'TikTok',
target_price: 24.99,
target_currency: 'USD',
profit_snapshot: {
netProfit: 5.8,
profitRate: 0.23,
roi: 0.32,
platformFee: 3.5,
logisticsCost: 2.5,
taxCost: 1.0,
exchangeRate: 0.14
},
risk_level: 'HIGH',
opportunity_score: 55,
status: 'PENDING',
execution_mode: 'MANUAL',
created_at: '2026-03-19T08:00:00Z',
updated_at: '2026-03-19T08:00:00Z'
},
{
id: 'arb_005',
tenant_id: 'tenant_001',
product_id: 'prod_005',
product_name: 'Portable Bluetooth Speaker Mini',
source_platform: 'Taobao',
source_url: 'https://taobao.com/item/33333',
source_price: 28,
source_currency: 'CNY',
target_platform: 'Shopee',
target_price: 12.99,
target_currency: 'USD',
profit_snapshot: {
netProfit: 3.2,
profitRate: 0.25,
roi: 0.38,
platformFee: 2.0,
logisticsCost: 1.5,
taxCost: 0.5,
exchangeRate: 0.14
},
risk_level: 'MEDIUM',
opportunity_score: 62,
status: 'REJECTED',
execution_mode: 'MANUAL',
created_at: '2026-03-17T16:00:00Z',
updated_at: '2026-03-18T10:00:00Z',
error_message: 'Profit margin below threshold'
}
];
const mockStats: ArbitrageStats = {
totalOpportunities: 15,
pendingOpportunities: 5,
executedOpportunities: 8,
successRate: 0.67,
totalProfit: 256.8,
avgProfitMargin: 0.32,
avgROI: 0.56,
byPlatform: {
Amazon: { count: 6, profit: 120.5 },
Temu: { count: 4, profit: 45.2 },
TikTok: { count: 3, profit: 58.3 },
Shopee: { count: 2, profit: 32.8 }
},
byStatus: {
PENDING: 5,
APPROVED: 2,
EXECUTED: 6,
REJECTED: 2
}
};
class MockArbitrageDataSource implements IArbitrageDataSource {
private opportunities = [...mockOpportunities];
async getOpportunities(params?: OpportunityQueryParams): Promise<{ opportunities: ArbitrageOpportunity[]; total: number }> {
await this.delay(300);
let filtered = [...this.opportunities];
if (params?.status) {
filtered = filtered.filter(o => o.status === params.status);
}
if (params?.platform) {
filtered = filtered.filter(o => o.target_platform === params.platform);
}
if (params?.minScore) {
filtered = filtered.filter(o => o.opportunity_score >= params.minScore!);
}
const total = filtered.length;
const offset = params?.offset || 0;
const limit = params?.limit || 50;
const opportunities = filtered.slice(offset, offset + limit);
return { opportunities, total };
}
async getOpportunityById(id: string): Promise<ArbitrageOpportunity | null> {
await this.delay(200);
return this.opportunities.find(o => o.id === id) || null;
}
async createOpportunity(params: CreateOpportunityParams): Promise<ArbitrageOpportunity> {
await this.delay(500);
const opportunity: ArbitrageOpportunity = {
id: `arb_${Date.now()}`,
tenant_id: 'tenant_001',
product_id: params.productId,
product_name: params.productName,
source_platform: params.sourcePlatform,
source_url: params.sourceUrl,
source_price: params.sourcePrice,
source_currency: params.sourceCurrency,
target_platform: params.targetPlatform,
target_price: params.targetPrice,
target_currency: params.targetCurrency,
profit_snapshot: {
netProfit: (params.targetPrice - params.sourcePrice * 0.14) * 0.6,
profitRate: 0.35,
roi: 0.5,
platformFee: params.targetPrice * 0.15,
logisticsCost: params.logisticsCost || 3,
taxCost: params.targetPrice * 0.05,
exchangeRate: 0.14
},
risk_level: 'MEDIUM',
opportunity_score: Math.floor(Math.random() * 30) + 60,
status: 'PENDING',
execution_mode: params.executionMode || 'MANUAL',
created_at: new Date().toISOString(),
updated_at: new Date().toISOString()
};
this.opportunities.unshift(opportunity);
return opportunity;
}
async approveOpportunity(id: string): Promise<ArbitrageOpportunity> {
await this.delay(300);
const opportunity = this.opportunities.find(o => o.id === id);
if (!opportunity) throw new Error('Opportunity not found');
opportunity.status = 'APPROVED';
opportunity.updated_at = new Date().toISOString();
return opportunity;
}
async rejectOpportunity(id: string, reason: string): Promise<ArbitrageOpportunity> {
await this.delay(300);
const opportunity = this.opportunities.find(o => o.id === id);
if (!opportunity) throw new Error('Opportunity not found');
opportunity.status = 'REJECTED';
opportunity.error_message = reason;
opportunity.updated_at = new Date().toISOString();
return opportunity;
}
async executeOpportunity(id: string): Promise<ArbitrageExecution> {
await this.delay(500);
const opportunity = this.opportunities.find(o => o.id === id);
if (!opportunity) throw new Error('Opportunity not found');
opportunity.status = 'EXECUTING';
opportunity.updated_at = new Date().toISOString();
const execution: ArbitrageExecution = {
id: `exec_${Date.now()}`,
opportunity_id: id,
tenant_id: opportunity.tenant_id,
status: 'STARTED',
created_at: new Date().toISOString(),
updated_at: new Date().toISOString()
};
return execution;
}
async scanProducts(params: ScanParams): Promise<ArbitrageOpportunity[]> {
await this.delay(1000);
const newOpportunities: ArbitrageOpportunity[] = [];
const productIds = params.productIds.split('\n').filter(id => id.trim());
for (const productId of productIds) {
const opportunity: ArbitrageOpportunity = {
id: `arb_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
tenant_id: 'tenant_001',
product_id: productId.trim(),
product_name: `Product ${productId.trim()}`,
source_platform: params.sourcePlatform,
source_url: `https://${params.sourcePlatform.toLowerCase()}.com/item/${productId}`,
source_price: Math.floor(Math.random() * 100) + 10,
source_currency: 'CNY',
target_platform: ['Amazon', 'Temu', 'TikTok'][Math.floor(Math.random() * 3)],
target_price: Math.floor(Math.random() * 50) + 15,
target_currency: 'USD',
profit_snapshot: {
netProfit: Math.random() * 20 + 5,
profitRate: Math.random() * 0.3 + 0.2,
roi: Math.random() * 0.5 + 0.3,
platformFee: Math.random() * 5 + 2,
logisticsCost: 3,
taxCost: 1,
exchangeRate: 0.14
},
risk_level: ['LOW', 'MEDIUM', 'HIGH'][Math.floor(Math.random() * 3)] as any,
opportunity_score: Math.floor(Math.random() * 40) + 50,
status: 'PENDING',
execution_mode: 'MANUAL',
created_at: new Date().toISOString(),
updated_at: new Date().toISOString()
};
newOpportunities.push(opportunity);
this.opportunities.unshift(opportunity);
}
return newOpportunities;
}
async getStats(): Promise<ArbitrageStats> {
await this.delay(200);
return mockStats;
}
async getExecutions(opportunityId: string): Promise<ArbitrageExecution[]> {
await this.delay(200);
return [
{
id: 'exec_001',
opportunity_id: opportunityId,
tenant_id: 'tenant_001',
status: 'COMPLETED',
purchase_order_id: 'po_12345',
listing_id: 'list_67890',
sale_order_id: 'so_11111',
actual_purchase_price: 35,
actual_sale_price: 29.99,
actual_profit: 12.5,
notes: 'Successfully completed arbitrage',
created_at: '2026-03-18T14:00:00Z',
updated_at: '2026-03-18T18:00:00Z'
}
];
}
private delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
class ApiArbitrageDataSource implements IArbitrageDataSource {
private baseUrl = '/api/v1/arbitrage';
async getOpportunities(params?: OpportunityQueryParams): Promise<{ opportunities: ArbitrageOpportunity[]; total: number }> {
const query = new URLSearchParams();
if (params?.status) query.append('status', params.status);
if (params?.platform) query.append('platform', params.platform);
if (params?.minScore) query.append('minScore', params.minScore.toString());
if (params?.limit) query.append('limit', params.limit.toString());
if (params?.offset) query.append('offset', params.offset.toString());
const response = await fetch(`${this.baseUrl}/opportunities?${query.toString()}`);
if (!response.ok) throw new Error(`API Error: ${response.status}`);
const result = await response.json();
return result.data || { opportunities: [], total: 0 };
}
async getOpportunityById(id: string): Promise<ArbitrageOpportunity | null> {
const response = await fetch(`${this.baseUrl}/opportunities/${id}`);
if (!response.ok) {
if (response.status === 404) return null;
throw new Error(`API Error: ${response.status}`);
}
const result = await response.json();
return result.data || null;
}
async createOpportunity(params: CreateOpportunityParams): Promise<ArbitrageOpportunity> {
const response = await fetch(`${this.baseUrl}/opportunities`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(params)
});
if (!response.ok) throw new Error(`API Error: ${response.status}`);
const result = await response.json();
return result.data;
}
async approveOpportunity(id: string): Promise<ArbitrageOpportunity> {
const response = await fetch(`${this.baseUrl}/opportunities/${id}/approve`, {
method: 'POST'
});
if (!response.ok) throw new Error(`API Error: ${response.status}`);
const result = await response.json();
return result.data;
}
async rejectOpportunity(id: string, reason: string): Promise<ArbitrageOpportunity> {
const response = await fetch(`${this.baseUrl}/opportunities/${id}/reject`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ reason })
});
if (!response.ok) throw new Error(`API Error: ${response.status}`);
const result = await response.json();
return result.data;
}
async executeOpportunity(id: string): Promise<ArbitrageExecution> {
const response = await fetch(`${this.baseUrl}/opportunities/${id}/execute`, {
method: 'POST'
});
if (!response.ok) throw new Error(`API Error: ${response.status}`);
const result = await response.json();
return result.data;
}
async scanProducts(params: ScanParams): Promise<ArbitrageOpportunity[]> {
const response = await fetch(`${this.baseUrl}/scan`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(params)
});
if (!response.ok) throw new Error(`API Error: ${response.status}`);
const result = await response.json();
return result.data || [];
}
async getStats(): Promise<ArbitrageStats> {
const response = await fetch(`${this.baseUrl}/stats`);
if (!response.ok) throw new Error(`API Error: ${response.status}`);
const result = await response.json();
return result.data;
}
async getExecutions(opportunityId: string): Promise<ArbitrageExecution[]> {
const response = await fetch(`${this.baseUrl}/opportunities/${opportunityId}/executions`);
if (!response.ok) throw new Error(`API Error: ${response.status}`);
const result = await response.json();
return result.data || [];
}
}
const arbitrageDataSource: IArbitrageDataSource = USE_MOCK
? new MockArbitrageDataSource()
: new ApiArbitrageDataSource();
export default arbitrageDataSource;
export { arbitrageDataSource };

View File

@@ -0,0 +1,520 @@
/**
* [MOCK] AI决策自动化配置数据源
* AI注意: 这是Mock实现不是真实业务逻辑
* 仅在USE_MOCK=true时启用
*/
export type AutomationLevel = 'L1' | 'L2' | 'L3' | 'L4';
export type RiskLevel = 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL';
export type DecisionModule =
| 'PRICING'
| 'INVENTORY'
| 'AD_OPTIMIZE'
| 'PRODUCT_SELECT'
| 'LOGISTICS'
| 'RISK_CONTROL'
| 'CUSTOMER_SERVICE'
| 'SETTLEMENT';
export interface AutoExecutionConfig {
id: string;
tenant_id: string;
shop_id: string;
module: DecisionModule;
automation_level: AutomationLevel;
confidence_thresholds: {
auto_execute: number;
pending_review: number;
auto_reject: number;
};
risk_limits: {
max_amount: number;
max_quantity: number;
allowed_risk_levels: RiskLevel[];
};
time_restrictions: {
allowed_hours: number[];
excluded_dates: string[];
};
rollback_config: {
enabled: boolean;
max_attempts: number;
cooldown_minutes: number;
};
notification_config: {
on_execute: boolean;
on_fail: boolean;
on_rollback: boolean;
channels: ('EMAIL' | 'SMS' | 'WEBHOOK')[];
};
statistics: {
total_executions: number;
success_count: number;
fail_count: number;
rollback_count: number;
avg_confidence: number;
last_execution_at?: string;
};
status: 'ACTIVE' | 'INACTIVE' | 'SUSPENDED';
created_by: string;
updated_by: string;
created_at: string;
updated_at: string;
}
export interface LevelEvolutionRecord {
id: string;
tenant_id: string;
shop_id: string;
module: DecisionModule;
from_level: AutomationLevel;
to_level: AutomationLevel;
reason: string;
metrics: {
success_rate: number;
total_executions: number;
avg_confidence: number;
};
approved_by: string;
created_at: string;
}
export interface LevelCapability {
max_risk_level: RiskLevel;
max_amount_multiplier: number;
requires_approval: boolean;
description: string;
}
export interface IAutoExecutionDataSource {
fetchConfigs(tenantId: string, shopId?: string): Promise<AutoExecutionConfig[]>;
fetchConfig(tenantId: string, shopId: string, module: DecisionModule): Promise<AutoExecutionConfig>;
updateConfig(configId: string, updates: Partial<AutoExecutionConfig>): Promise<AutoExecutionConfig>;
upgradeLevel(tenantId: string, shopId: string, module: DecisionModule, targetLevel: AutomationLevel, reason: string): Promise<LevelEvolutionRecord>;
downgradeLevel(tenantId: string, shopId: string, module: DecisionModule, targetLevel: AutomationLevel, reason: string): Promise<LevelEvolutionRecord>;
fetchEvolutionHistory(tenantId: string, shopId?: string, module?: DecisionModule): Promise<LevelEvolutionRecord[]>;
fetchLevelCapabilities(): Promise<Record<AutomationLevel, LevelCapability>>;
fetchModuleDefaults(): Promise<Record<DecisionModule, Partial<AutoExecutionConfig>>>;
}
const MODULE_NAMES: Record<DecisionModule, string> = {
PRICING: '定价模块',
INVENTORY: '库存模块',
AD_OPTIMIZE: '广告优化',
PRODUCT_SELECT: '选品模块',
LOGISTICS: '物流模块',
RISK_CONTROL: '风控模块',
CUSTOMER_SERVICE: '客服模块',
SETTLEMENT: '结算模块',
};
const LEVEL_CAPABILITIES: Record<AutomationLevel, LevelCapability> = {
L1: {
max_risk_level: 'LOW',
max_amount_multiplier: 0.5,
requires_approval: true,
description: '仅建议,所有操作需人工确认',
},
L2: {
max_risk_level: 'MEDIUM',
max_amount_multiplier: 1.0,
requires_approval: false,
description: '低风险操作可自动执行,中高风险需审核',
},
L3: {
max_risk_level: 'HIGH',
max_amount_multiplier: 2.0,
requires_approval: false,
description: '大部分操作可自动执行,仅高风险需审核',
},
L4: {
max_risk_level: 'CRITICAL',
max_amount_multiplier: 5.0,
requires_approval: false,
description: '全自动化,包含关键操作,需严格监控',
},
};
const MODULE_DEFAULTS: Record<DecisionModule, Partial<AutoExecutionConfig>> = {
PRICING: {
confidence_thresholds: { auto_execute: 0.85, pending_review: 0.60, auto_reject: 0.40 },
risk_limits: { max_amount: 5000, max_quantity: 100, allowed_risk_levels: ['LOW', 'MEDIUM'] },
},
INVENTORY: {
confidence_thresholds: { auto_execute: 0.90, pending_review: 0.70, auto_reject: 0.50 },
risk_limits: { max_amount: 10000, max_quantity: 500, allowed_risk_levels: ['LOW', 'MEDIUM'] },
},
AD_OPTIMIZE: {
confidence_thresholds: { auto_execute: 0.80, pending_review: 0.55, auto_reject: 0.35 },
risk_limits: { max_amount: 2000, max_quantity: 50, allowed_risk_levels: ['LOW', 'MEDIUM', 'HIGH'] },
},
PRODUCT_SELECT: {
confidence_thresholds: { auto_execute: 0.75, pending_review: 0.50, auto_reject: 0.30 },
risk_limits: { max_amount: 3000, max_quantity: 200, allowed_risk_levels: ['LOW', 'MEDIUM'] },
},
LOGISTICS: {
confidence_thresholds: { auto_execute: 0.88, pending_review: 0.65, auto_reject: 0.45 },
risk_limits: { max_amount: 1000, max_quantity: 100, allowed_risk_levels: ['LOW', 'MEDIUM'] },
},
RISK_CONTROL: {
confidence_thresholds: { auto_execute: 0.95, pending_review: 0.80, auto_reject: 0.60 },
risk_limits: { max_amount: 50000, max_quantity: 1000, allowed_risk_levels: ['LOW'] },
},
CUSTOMER_SERVICE: {
confidence_thresholds: { auto_execute: 0.82, pending_review: 0.60, auto_reject: 0.40 },
risk_limits: { max_amount: 500, max_quantity: 10, allowed_risk_levels: ['LOW', 'MEDIUM'] },
},
SETTLEMENT: {
confidence_thresholds: { auto_execute: 0.98, pending_review: 0.90, auto_reject: 0.70 },
risk_limits: { max_amount: 100000, max_quantity: 100, allowed_risk_levels: ['LOW'] },
},
};
class MockAutoExecutionDataSource implements IAutoExecutionDataSource {
private configs: AutoExecutionConfig[] = [];
private evolutionRecords: LevelEvolutionRecord[] = [];
constructor() {
this.initMockData();
}
private initMockData() {
const modules: DecisionModule[] = [
'PRICING', 'INVENTORY', 'AD_OPTIMIZE', 'PRODUCT_SELECT',
'LOGISTICS', 'RISK_CONTROL', 'CUSTOMER_SERVICE', 'SETTLEMENT'
];
this.configs = modules.map((module, index) => {
const defaults = MODULE_DEFAULTS[module];
return {
id: `AEC-${index + 1}`,
tenant_id: 'tenant-001',
shop_id: 'shop-001',
module,
automation_level: (['L1', 'L2', 'L3', 'L4'] as AutomationLevel[])[index % 4],
confidence_thresholds: defaults.confidence_thresholds!,
risk_limits: defaults.risk_limits!,
time_restrictions: {
allowed_hours: Array.from({ length: 24 }, (_, i) => i),
excluded_dates: [],
},
rollback_config: {
enabled: true,
max_attempts: 3,
cooldown_minutes: 30,
},
notification_config: {
on_execute: true,
on_fail: true,
on_rollback: true,
channels: ['EMAIL'] as ('EMAIL' | 'SMS' | 'WEBHOOK')[],
},
statistics: {
total_executions: Math.floor(Math.random() * 1000) + 100,
success_count: Math.floor(Math.random() * 900) + 90,
fail_count: Math.floor(Math.random() * 50) + 5,
rollback_count: Math.floor(Math.random() * 20) + 2,
avg_confidence: 0.75 + Math.random() * 0.2,
last_execution_at: new Date(Date.now() - Math.random() * 86400000).toISOString(),
},
status: 'ACTIVE',
created_by: 'admin',
updated_by: 'admin',
created_at: new Date(Date.now() - Math.random() * 30 * 86400000).toISOString(),
updated_at: new Date().toISOString(),
};
});
this.evolutionRecords = [
{
id: 'EVO-001',
tenant_id: 'tenant-001',
shop_id: 'shop-001',
module: 'PRICING',
from_level: 'L1',
to_level: 'L2',
reason: '成功率达标,自动升级',
metrics: {
success_rate: 0.96,
total_executions: 150,
avg_confidence: 0.88,
},
approved_by: 'admin',
created_at: new Date(Date.now() - 7 * 86400000).toISOString(),
},
];
}
async fetchConfigs(tenantId: string, shopId?: string): Promise<AutoExecutionConfig[]> {
await this.delay(300);
return this.configs.filter(c =>
c.tenant_id === tenantId &&
(!shopId || c.shop_id === shopId)
);
}
async fetchConfig(tenantId: string, shopId: string, module: DecisionModule): Promise<AutoExecutionConfig> {
await this.delay(200);
const config = this.configs.find(c =>
c.tenant_id === tenantId &&
c.shop_id === shopId &&
c.module === module
);
if (!config) {
const defaults = MODULE_DEFAULTS[module];
return {
id: `AEC-NEW-${Date.now()}`,
tenant_id: tenantId,
shop_id: shopId,
module,
automation_level: 'L1',
confidence_thresholds: defaults.confidence_thresholds!,
risk_limits: defaults.risk_limits!,
time_restrictions: {
allowed_hours: Array.from({ length: 24 }, (_, i) => i),
excluded_dates: [],
},
rollback_config: {
enabled: true,
max_attempts: 3,
cooldown_minutes: 30,
},
notification_config: {
on_execute: true,
on_fail: true,
on_rollback: true,
channels: ['EMAIL'],
},
statistics: {
total_executions: 0,
success_count: 0,
fail_count: 0,
rollback_count: 0,
avg_confidence: 0,
},
status: 'ACTIVE',
created_by: 'SYSTEM',
updated_by: 'SYSTEM',
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
};
}
return config;
}
async updateConfig(configId: string, updates: Partial<AutoExecutionConfig>): Promise<AutoExecutionConfig> {
await this.delay(300);
const index = this.configs.findIndex(c => c.id === configId);
if (index === -1) {
throw new Error('Config not found');
}
this.configs[index] = {
...this.configs[index],
...updates,
updated_at: new Date().toISOString(),
};
return this.configs[index];
}
async upgradeLevel(
tenantId: string,
shopId: string,
module: DecisionModule,
targetLevel: AutomationLevel,
reason: string
): Promise<LevelEvolutionRecord> {
await this.delay(500);
const config = await this.fetchConfig(tenantId, shopId, module);
const record: LevelEvolutionRecord = {
id: `EVO-${Date.now()}`,
tenant_id: tenantId,
shop_id: shopId,
module,
from_level: config.automation_level,
to_level: targetLevel,
reason,
metrics: {
success_rate: config.statistics.total_executions > 0
? config.statistics.success_count / config.statistics.total_executions
: 0,
total_executions: config.statistics.total_executions,
avg_confidence: config.statistics.avg_confidence,
},
approved_by: 'admin',
created_at: new Date().toISOString(),
};
this.evolutionRecords.unshift(record);
const index = this.configs.findIndex(c => c.module === module);
if (index !== -1) {
this.configs[index].automation_level = targetLevel;
}
return record;
}
async downgradeLevel(
tenantId: string,
shopId: string,
module: DecisionModule,
targetLevel: AutomationLevel,
reason: string
): Promise<LevelEvolutionRecord> {
await this.delay(500);
const config = await this.fetchConfig(tenantId, shopId, module);
const record: LevelEvolutionRecord = {
id: `EVO-${Date.now()}`,
tenant_id: tenantId,
shop_id: shopId,
module,
from_level: config.automation_level,
to_level: targetLevel,
reason,
metrics: {
success_rate: config.statistics.total_executions > 0
? config.statistics.success_count / config.statistics.total_executions
: 0,
total_executions: config.statistics.total_executions,
avg_confidence: config.statistics.avg_confidence,
},
approved_by: 'admin',
created_at: new Date().toISOString(),
};
this.evolutionRecords.unshift(record);
const index = this.configs.findIndex(c => c.module === module);
if (index !== -1) {
this.configs[index].automation_level = targetLevel;
}
return record;
}
async fetchEvolutionHistory(
tenantId: string,
shopId?: string,
module?: DecisionModule
): Promise<LevelEvolutionRecord[]> {
await this.delay(200);
return this.evolutionRecords.filter(r =>
r.tenant_id === tenantId &&
(!shopId || r.shop_id === shopId) &&
(!module || r.module === module)
);
}
async fetchLevelCapabilities(): Promise<Record<AutomationLevel, LevelCapability>> {
await this.delay(100);
return LEVEL_CAPABILITIES;
}
async fetchModuleDefaults(): Promise<Record<DecisionModule, Partial<AutoExecutionConfig>>> {
await this.delay(100);
return MODULE_DEFAULTS;
}
private delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
class ApiAutoExecutionDataSource implements IAutoExecutionDataSource {
private baseUrl = '/api/auto-execution';
async fetchConfigs(tenantId: string, shopId?: string): Promise<AutoExecutionConfig[]> {
const params = new URLSearchParams({ tenant_id: tenantId });
if (shopId) params.append('shop_id', shopId);
const response = await fetch(`${this.baseUrl}/configs?${params}`);
if (!response.ok) throw new Error('Failed to fetch configs');
const data = await response.json();
return data.data;
}
async fetchConfig(tenantId: string, shopId: string, module: DecisionModule): Promise<AutoExecutionConfig> {
const params = new URLSearchParams({ tenant_id: tenantId, shop_id: shopId });
const response = await fetch(`${this.baseUrl}/config/${module}?${params}`);
if (!response.ok) throw new Error('Failed to fetch config');
const data = await response.json();
return data.data;
}
async updateConfig(configId: string, updates: Partial<AutoExecutionConfig>): Promise<AutoExecutionConfig> {
const response = await fetch(`${this.baseUrl}/config/${configId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(updates),
});
if (!response.ok) throw new Error('Failed to update config');
const data = await response.json();
return data.data;
}
async upgradeLevel(
tenantId: string,
shopId: string,
module: DecisionModule,
targetLevel: AutomationLevel,
reason: string
): Promise<LevelEvolutionRecord> {
const response = await fetch(`${this.baseUrl}/level/upgrade`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ tenant_id: tenantId, shop_id: shopId, module, target_level: targetLevel, reason }),
});
if (!response.ok) throw new Error('Failed to upgrade level');
const data = await response.json();
return data.data;
}
async downgradeLevel(
tenantId: string,
shopId: string,
module: DecisionModule,
targetLevel: AutomationLevel,
reason: string
): Promise<LevelEvolutionRecord> {
const response = await fetch(`${this.baseUrl}/level/downgrade`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ tenant_id: tenantId, shop_id: shopId, module, target_level: targetLevel, reason }),
});
if (!response.ok) throw new Error('Failed to downgrade level');
const data = await response.json();
return data.data;
}
async fetchEvolutionHistory(
tenantId: string,
shopId?: string,
module?: DecisionModule
): Promise<LevelEvolutionRecord[]> {
const params = new URLSearchParams({ tenant_id: tenantId });
if (shopId) params.append('shop_id', shopId);
if (module) params.append('module', module);
const response = await fetch(`${this.baseUrl}/evolution-history?${params}`);
if (!response.ok) throw new Error('Failed to fetch evolution history');
const data = await response.json();
return data.data;
}
async fetchLevelCapabilities(): Promise<Record<AutomationLevel, LevelCapability>> {
const response = await fetch(`${this.baseUrl}/level-capabilities`);
if (!response.ok) throw new Error('Failed to fetch level capabilities');
const data = await response.json();
return data.data;
}
async fetchModuleDefaults(): Promise<Record<DecisionModule, Partial<AutoExecutionConfig>>> {
const response = await fetch(`${this.baseUrl}/module-defaults`);
if (!response.ok) throw new Error('Failed to fetch module defaults');
const data = await response.json();
return data.data;
}
}
const useMock = process.env.REACT_APP_USE_MOCK === 'true';
export const autoExecutionDataSource: IAutoExecutionDataSource = useMock
? new MockAutoExecutionDataSource()
: new ApiAutoExecutionDataSource();
export { MODULE_NAMES, LEVEL_CAPABILITIES, MODULE_DEFAULTS };

View File

@@ -10,6 +10,7 @@
import { Certificate } from '@/types/certificate';
import { IDataSource, CertificateQueryParams } from '@/types/datasource';
import { BaseDataSource, BaseMockDataSource, DataSourceFactory } from './dataSourceFactory';
// ============================================
// 真实API实现
@@ -19,104 +20,9 @@ import { IDataSource, CertificateQueryParams } from '@/types/datasource';
* 证书API数据源
* 调用真实后端API
*/
class ApiCertificateDataSource implements IDataSource<Certificate, CertificateQueryParams> {
private baseUrl = '/api/v1/certificate';
async list(params?: CertificateQueryParams): Promise<Certificate[]> {
const query = new URLSearchParams();
if (params?.status) query.append('status', params.status);
if (params?.type) query.append('type', params.type);
if (params?.keyword) query.append('keyword', params.keyword);
if (params?.page) query.append('page', params.page.toString());
if (params?.pageSize) query.append('pageSize', params.pageSize.toString());
const response = await fetch(`${this.baseUrl}/certificates?${query.toString()}`, {
headers: {
'Content-Type': 'application/json',
},
});
if (!response.ok) {
throw new Error(`API Error: ${response.status}`);
}
const result = await response.json();
return result.data || [];
}
async detail(id: string): Promise<Certificate | null> {
const response = await fetch(`${this.baseUrl}/certificates/${id}`, {
headers: {
'Content-Type': 'application/json',
},
});
if (!response.ok) {
if (response.status === 404) return null;
throw new Error(`API Error: ${response.status}`);
}
const result = await response.json();
return result.data || null;
}
async create(data: Partial<Certificate>): Promise<Certificate> {
const response = await fetch(`${this.baseUrl}/certificates`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
});
if (!response.ok) {
throw new Error(`API Error: ${response.status}`);
}
const result = await response.json();
// 创建成功后获取完整数据
if (result.data?.id) {
const created = await this.detail(result.data.id);
if (created) return created;
}
return result.data;
}
async update(id: string, data: Partial<Certificate>): Promise<Certificate> {
const response = await fetch(`${this.baseUrl}/certificates/${id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
});
if (!response.ok) {
throw new Error(`API Error: ${response.status}`);
}
const result = await response.json();
// 更新成功后获取完整数据
const updated = await this.detail(id);
if (updated) return updated;
return result.data;
}
async delete(id: string): Promise<void> {
const response = await fetch(`${this.baseUrl}/certificates/${id}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
},
});
if (!response.ok) {
throw new Error(`API Error: ${response.status}`);
}
class ApiCertificateDataSource extends BaseDataSource<Certificate, CertificateQueryParams> {
constructor() {
super('/api/v1/certificate/certificates');
}
/**
@@ -126,7 +32,7 @@ class ApiCertificateDataSource implements IDataSource<Certificate, CertificateQu
* @param approvedBy 审核人
*/
async updateStatus(id: string, status: string, approvedBy?: string): Promise<Certificate> {
const response = await fetch(`${this.baseUrl}/certificates/${id}/status`, {
const response = await fetch(`${this.baseUrl}/${id}/status`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
@@ -157,13 +63,12 @@ class ApiCertificateDataSource implements IDataSource<Certificate, CertificateQu
* AI注意: 这是Mock实现不是真实业务逻辑
* 仅在REACT_APP_USE_MOCK=true时启用
*/
class MockCertificateDataSource implements IDataSource<Certificate, CertificateQueryParams> {
/** Mock标记 */
readonly __MOCK__ = true as const;
class MockCertificateDataSource extends BaseMockDataSource<Certificate, CertificateQueryParams> {
/** Mock数据源名称 */
readonly __MOCK_NAME__ = 'MockCertificateDataSource';
private mockData: Certificate[] = [
/** Mock数据 */
protected mockData: Certificate[] = [
{
id: '1',
name: 'CE认证证书',
@@ -248,89 +153,12 @@ class MockCertificateDataSource implements IDataSource<Certificate, CertificateQ
},
];
private delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
async list(params?: CertificateQueryParams): Promise<Certificate[]> {
// 模拟网络延迟
await this.delay(300);
let result = [...this.mockData];
// 状态筛选
if (params?.status) {
result = result.filter(item => item.status === params.status);
}
// 类型筛选
if (params?.type) {
result = result.filter(item => item.type === params.type);
}
// 关键词搜索
if (params?.keyword) {
const keyword = params.keyword.toLowerCase();
result = result.filter(
item =>
item.name.toLowerCase().includes(keyword) ||
item.productName?.toLowerCase().includes(keyword) ||
item.notes?.toLowerCase().includes(keyword)
);
}
// 分页
const page = params?.page || 1;
const pageSize = params?.pageSize || 10;
const start = (page - 1) * pageSize;
const end = start + pageSize;
return result.slice(start, end);
}
async detail(id: string): Promise<Certificate | null> {
await this.delay(200);
return this.mockData.find(item => item.id === id) || null;
}
async create(data: Partial<Certificate>): Promise<Certificate> {
await this.delay(500);
const newCert: Certificate = {
id: `${Date.now()}`,
name: data.name || '',
type: data.type || 'OTHER',
status: 'PENDING',
fileUrl: data.fileUrl || '/files/uploaded.pdf',
fileName: data.fileName || 'uploaded.pdf',
uploadDate: new Date().toISOString().split('T')[0],
expiryDate: data.expiryDate || '',
productId: data.productId,
productName: data.productName,
notes: data.notes,
};
this.mockData.unshift(newCert);
return newCert;
}
async update(id: string, data: Partial<Certificate>): Promise<Certificate> {
await this.delay(300);
const index = this.mockData.findIndex(item => item.id === id);
if (index === -1) {
throw new Error('Certificate not found');
}
this.mockData[index] = { ...this.mockData[index], ...data };
return this.mockData[index];
}
async delete(id: string): Promise<void> {
await this.delay(200);
this.mockData = this.mockData.filter(item => item.id !== id);
}
/**
* 更新证书状态(审核)
* @param id 证书ID
* @param status 新状态
* @param approvedBy 审核人
*/
async updateStatus(id: string, status: string, approvedBy?: string): Promise<Certificate> {
await this.delay(300);
@@ -354,40 +182,22 @@ class MockCertificateDataSource implements IDataSource<Certificate, CertificateQ
}
// ============================================
// 导出数据源实例 (环境变量控制)
// 导出数据源
// ============================================
const useMock = process.env.REACT_APP_USE_MOCK === 'true';
/**
* 证书数据源实例
* 根据环境变量自动切换Mock/真实API
*
* 使用示例:
* ```typescript
* import { certificateDataSource } from '@/services/certificateDataSource';
*
* // 查询列表
* const certificates = await certificateDataSource.list({ status: 'APPROVED' });
*
* // 获取详情
* const cert = await certificateDataSource.detail('1');
*
* // 创建
* const newCert = await certificateDataSource.create({ name: '新证书', ... });
* ```
*/
export const certificateDataSource: IDataSource<Certificate, CertificateQueryParams> & {
updateStatus?(id: string, status: string, approvedBy?: string): Promise<Certificate>;
} = useMock ? new MockCertificateDataSource() : new ApiCertificateDataSource();
export const certificateDataSource = DataSourceFactory.createWithMethods<
Certificate,
CertificateQueryParams,
{
updateStatus(id: string, status: string, approvedBy?: string): Promise<Certificate>;
}
>({
apiDataSource: ApiCertificateDataSource,
mockDataSource: MockCertificateDataSource,
});
/**
* Mock状态标记
* 用于调试和开发环境识别
*/
export const __MOCK__ = useMock;
/**
* 当前数据源类型
*/
export const __DATA_SOURCE_TYPE__ = useMock ? 'mock' : 'api';
export { __MOCK__, __DATA_SOURCE_TYPE__ } from './dataSourceFactory';

View File

@@ -0,0 +1,184 @@
/**
* DataSourceFactory 单元测试
* 测试 DataSource 工厂模式的核心功能
*/
import { DataSourceFactory, BaseDataSource, BaseMockDataSource } from '../src/services/dataSourceFactory';
// 测试接口
interface TestItem {
id: string;
name: string;
value: number;
}
interface TestQueryParams {
name?: string;
value?: number;
}
// 测试 API DataSource
class TestApiDataSource extends BaseDataSource<TestItem, TestQueryParams> {
constructor() {
super('/api/test');
}
// 自定义方法
async customMethod(): Promise<string> {
return 'API custom method';
}
}
// 测试 Mock DataSource
class TestMockDataSource extends BaseMockDataSource<TestItem, TestQueryParams> {
readonly __MOCK_NAME__ = 'TestMockDataSource';
protected mockData: TestItem[] = [
{ id: '1', name: 'Test 1', value: 100 },
{ id: '2', name: 'Test 2', value: 200 },
];
// 自定义方法
async customMethod(): Promise<string> {
return 'Mock custom method';
}
}
describe('DataSourceFactory', () => {
describe('create', () => {
it('should create DataSource instance', () => {
const dataSource = DataSourceFactory.create(
TestApiDataSource,
TestMockDataSource
);
expect(dataSource).toBeDefined();
expect(typeof dataSource.list).toBe('function');
expect(typeof dataSource.detail).toBe('function');
expect(typeof dataSource.create).toBe('function');
expect(typeof dataSource.update).toBe('function');
expect(typeof dataSource.delete).toBe('function');
});
});
describe('createWithMethods', () => {
it('should create DataSource instance with custom methods', () => {
const dataSource = DataSourceFactory.createWithMethods<
TestItem,
TestQueryParams,
{
customMethod(): Promise<string>;
}
>({
apiDataSource: TestApiDataSource,
mockDataSource: TestMockDataSource,
});
expect(dataSource).toBeDefined();
expect(typeof dataSource.list).toBe('function');
expect(typeof dataSource.customMethod).toBe('function');
});
});
describe('BaseDataSource', () => {
let dataSource: TestApiDataSource;
beforeEach(() => {
dataSource = new TestApiDataSource();
});
describe('buildQueryParams', () => {
it('should build query params from object', () => {
const params = { name: 'test', value: 100, page: 1 };
const query = dataSource['buildQueryParams'](params);
expect(query).toBe('name=test&value=100&page=1');
});
it('should handle undefined params', () => {
const query = dataSource['buildQueryParams'](undefined);
expect(query).toBe('');
});
it('should handle null values', () => {
const params = { name: 'test', value: null };
const query = dataSource['buildQueryParams'](params);
expect(query).toBe('name=test');
});
});
});
describe('BaseMockDataSource', () => {
let dataSource: TestMockDataSource;
beforeEach(() => {
dataSource = new TestMockDataSource();
});
describe('list', () => {
it('should return mock data', async () => {
const items = await dataSource.list();
expect(items).toHaveLength(2);
expect(items[0].id).toBe('1');
expect(items[1].id).toBe('2');
});
});
describe('detail', () => {
it('should return item by id', async () => {
const item = await dataSource.detail('1');
expect(item).toBeDefined();
expect(item?.id).toBe('1');
expect(item?.name).toBe('Test 1');
});
it('should return null for non-existent id', async () => {
const item = await dataSource.detail('999');
expect(item).toBeNull();
});
});
describe('create', () => {
it('should create new item', async () => {
const newItem = await dataSource.create({ name: 'Test 3', value: 300 });
expect(newItem).toBeDefined();
expect(newItem.id).toBeDefined();
expect(newItem.name).toBe('Test 3');
expect(newItem.value).toBe(300);
// Verify item was added to mock data
const items = await dataSource.list();
expect(items).toHaveLength(3);
});
});
describe('update', () => {
it('should update existing item', async () => {
const updatedItem = await dataSource.update('1', { name: 'Updated Test 1', value: 150 });
expect(updatedItem).toBeDefined();
expect(updatedItem.id).toBe('1');
expect(updatedItem.name).toBe('Updated Test 1');
expect(updatedItem.value).toBe(150);
});
it('should throw error for non-existent id', async () => {
await expect(dataSource.update('999', { name: 'Test' })).rejects.toThrow('Item not found');
});
});
describe('delete', () => {
it('should delete item', async () => {
await dataSource.delete('1');
const items = await dataSource.list();
expect(items).toHaveLength(1);
expect(items[0].id).toBe('2');
});
});
describe('delay', () => {
it('should delay execution', async () => {
const startTime = Date.now();
await dataSource['delay'](100);
const endTime = Date.now();
expect(endTime - startTime).toBeGreaterThanOrEqual(100);
});
});
});
});

View File

@@ -0,0 +1,238 @@
/**
* 统一 DataSource 工厂模式
* 消除前端数据源重复代码,提供统一的创建和管理机制
*/
import { IDataSource } from '@/types/datasource';
// 环境变量判断
const useMock = process.env.REACT_APP_USE_MOCK === 'true';
/**
* 基础 DataSource 抽象类
* 提供通用的 CRUD 方法实现
*/
export abstract class BaseDataSource<T, P = any> implements IDataSource<T, P> {
protected baseUrl: string;
constructor(baseUrl: string) {
this.baseUrl = baseUrl;
}
async list(params?: P): Promise<T[]> {
const query = this.buildQueryParams(params);
const response = await fetch(`${this.baseUrl}?${query}`, {
headers: {
'Content-Type': 'application/json',
},
});
if (!response.ok) {
throw new Error(`API Error: ${response.status}`);
}
const result = await response.json();
return result.data || [];
}
async detail(id: string): Promise<T | null> {
const response = await fetch(`${this.baseUrl}/${id}`, {
headers: {
'Content-Type': 'application/json',
},
});
if (!response.ok) {
if (response.status === 404) return null;
throw new Error(`API Error: ${response.status}`);
}
const result = await response.json();
return result.data || null;
}
async create(data: Partial<T>): Promise<T> {
const response = await fetch(this.baseUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
});
if (!response.ok) {
throw new Error(`API Error: ${response.status}`);
}
const result = await response.json();
// 创建成功后获取完整数据
if (result.data?.id) {
const created = await this.detail(result.data.id);
if (created) return created;
}
return result.data;
}
async update(id: string, data: Partial<T>): Promise<T> {
const response = await fetch(`${this.baseUrl}/${id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
});
if (!response.ok) {
throw new Error(`API Error: ${response.status}`);
}
const result = await response.json();
// 更新成功后获取完整数据
const updated = await this.detail(id);
if (updated) return updated;
return result.data;
}
async delete(id: string): Promise<void> {
const response = await fetch(`${this.baseUrl}/${id}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
},
});
if (!response.ok) {
throw new Error(`API Error: ${response.status}`);
}
}
/**
* 构建查询参数
*/
protected buildQueryParams(params?: P): string {
const query = new URLSearchParams();
if (params) {
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined && value !== null) {
query.append(key, String(value));
}
});
}
return query.toString();
}
/**
* 模拟延迟(用于 Mock 实现)
*/
protected delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
/**
* 基础 Mock DataSource 类
*/
export abstract class BaseMockDataSource<T, P = any> implements IDataSource<T, P> {
/** Mock标记 */
readonly __MOCK__ = true as const;
/** Mock数据源名称 */
abstract readonly __MOCK_NAME__: string;
/** Mock数据 */
protected abstract mockData: T[];
async list(params?: P): Promise<T[]> {
await this.delay(200);
return this.mockData;
}
async detail(id: string): Promise<T | null> {
await this.delay(100);
return this.mockData.find(item => (item as any).id === id) || null;
}
async create(data: Partial<T>): Promise<T> {
await this.delay(300);
const newItem = {
id: `${Date.now()}`,
...data,
} as T;
this.mockData.unshift(newItem);
return newItem;
}
async update(id: string, data: Partial<T>): Promise<T> {
await this.delay(300);
const index = this.mockData.findIndex(item => (item as any).id === id);
if (index === -1) {
throw new Error('Item not found');
}
this.mockData[index] = { ...this.mockData[index], ...data };
return this.mockData[index];
}
async delete(id: string): Promise<void> {
await this.delay(200);
this.mockData = this.mockData.filter(item => (item as any).id !== id);
}
/**
* 模拟延迟
*/
protected delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
/**
* DataSource 工厂类
* 负责根据环境变量创建相应的 DataSource 实例
*/
export class DataSourceFactory {
/**
* 创建 DataSource 实例
* @param apiDataSource API实现类
* @param mockDataSource Mock实现类
* @returns DataSource 实例
*/
static create<T, P = any>(
apiDataSource: new () => IDataSource<T, P>,
mockDataSource: new () => IDataSource<T, P>
): IDataSource<T, P> {
return useMock ? new mockDataSource() : new apiDataSource();
}
/**
* 创建带有自定义方法的 DataSource 实例
* @param apiDataSource API实现类
* @param mockDataSource Mock实现类
* @returns DataSource 实例
*/
static createWithMethods<T, P = any, M extends Record<string, any>>({
apiDataSource,
mockDataSource,
}: {
apiDataSource: new () => IDataSource<T, P> & M;
mockDataSource: new () => IDataSource<T, P> & M;
}): IDataSource<T, P> & M {
return useMock ? new mockDataSource() : new apiDataSource();
}
}
/**
* Mock状态标记
* 用于调试和开发环境识别
*/
export const __MOCK__ = useMock;
/**
* 当前数据源类型
*/
export const __DATA_SOURCE_TYPE__ = useMock ? 'mock' : 'api';

View File

@@ -0,0 +1,573 @@
/**
* [MOCK] 动态定价数据源
* AI注意: 这是Mock实现不是真实业务逻辑
* 仅在USE_MOCK=true时启用
*/
export type PricingStrategy = 'GAME_THEORY' | 'COMPETITION' | 'DEMAND_BASED' | 'COST_PLUS' | 'DYNAMIC';
export type PriceAdjustmentDirection = 'INCREASE' | 'DECREASE' | 'MAINTAIN';
export type PricingDecisionStatus = 'PENDING' | 'EXECUTED' | 'REJECTED' | 'EXPIRED';
export interface PricingConfig {
id: string;
tenant_id: string;
shop_id: string;
product_id: string;
base_price: number;
min_price: number;
max_price: number;
cost_price: number;
strategy: PricingStrategy;
strategy_params: {
game_theory?: {
competitor_weight: number;
market_share_target: number;
profit_priority: number;
};
competition?: {
price_position: 'LOWEST' | 'BELOW_AVG' | 'AVG' | 'ABOVE_AVG' | 'HIGHEST';
max_diff_percent: number;
follow_leader: boolean;
};
demand_based?: {
elasticity_threshold: number;
demand_sensitivity: number;
seasonal_factor: number;
};
};
auto_adjust: boolean;
adjust_frequency: 'HOURLY' | 'DAILY' | 'WEEKLY';
max_adjust_percent: number;
cooldown_hours: number;
min_profit_margin: number;
min_profit_amount: number;
enabled: boolean;
created_at: string;
updated_at: string;
}
export interface PricingDecision {
id: string;
tenant_id: string;
shop_id: string;
product_id: string;
product_name?: string;
current_price: number;
suggested_price: number;
price_change: number;
change_percent: number;
adjustment_direction: PriceAdjustmentDirection;
strategy_used: PricingStrategy;
decision_factors: {
competitor_prices: number[];
market_avg_price: number;
demand_level: 'LOW' | 'MEDIUM' | 'HIGH';
inventory_level: number;
sales_velocity: number;
profit_margin: number;
};
predicted_impact: {
revenue_change: number;
profit_change: number;
sales_change: number;
confidence: number;
};
status: PricingDecisionStatus;
reviewed_by?: string;
reviewed_at?: string;
review_notes?: string;
executed_at?: string;
execution_result?: {
success: boolean;
actual_price?: number;
error?: string;
};
created_at: string;
expires_at: string;
}
export interface CompetitorPriceRecord {
id: string;
product_id: string;
competitor_shop_id: string;
competitor_name: string;
competitor_platform: string;
price: number;
shipping_cost: number;
total_price: number;
stock_status: 'IN_STOCK' | 'LOW_STOCK' | 'OUT_OF_STOCK';
rating: number;
review_count: number;
collected_at: string;
}
export interface CompetitorAnalysis {
product_id: string;
analysis_date: string;
price_stats: {
min_price: number;
max_price: number;
avg_price: number;
median_price: number;
our_price: number;
our_rank: number;
total_competitors: number;
};
competitor_rankings: Array<{
shop_id: string;
shop_name: string;
price: number;
rank: number;
price_diff: number;
rating: number;
review_count: number;
}>;
market_trend: {
price_trend: 'RISING' | 'FALLING' | 'STABLE';
competition_level: 'HIGH' | 'MEDIUM' | 'LOW';
recommended_action: 'INCREASE' | 'DECREASE' | 'MAINTAIN';
};
alerts: Array<{
type: 'PRICE_DROP' | 'OUT_OF_STOCK' | 'NEW_COMPETITOR' | 'PROMOTION';
severity: 'HIGH' | 'MEDIUM' | 'LOW';
message: string;
competitor_shop_id?: string;
}>;
}
export interface PricingABTest {
id: string;
tenant_id: string;
product_id: string;
test_name: string;
control_price: number;
variant_price: number;
traffic_split: number;
start_date: string;
end_date: string;
results?: {
control: { impressions: number; clicks: number; conversions: number; revenue: number };
variant: { impressions: number; clicks: number; conversions: number; revenue: number };
winner?: 'CONTROL' | 'VARIANT' | 'INCONCLUSIVE';
confidence?: number;
};
status: 'RUNNING' | 'COMPLETED' | 'PAUSED';
created_at: string;
}
export interface IDynamicPricingDataSource {
generatePricingDecision(productId: string, strategy?: PricingStrategy): Promise<PricingDecision>;
fetchPendingDecisions(limit?: number): Promise<PricingDecision[]>;
executePricingDecision(decisionId: string): Promise<{ success: boolean; message: string }>;
rejectPricingDecision(decisionId: string, reason: string): Promise<{ success: boolean }>;
fetchPricingConfig(productId: string): Promise<PricingConfig | null>;
updatePricingConfig(productId: string, config: Partial<PricingConfig>): Promise<PricingConfig>;
fetchCompetitorPrices(productId: string): Promise<CompetitorPriceRecord[]>;
analyzeCompetitorPrices(productId: string, ourPrice: number): Promise<CompetitorAnalysis>;
createABTest(params: {
productId: string;
testName: string;
controlPrice: number;
variantPrice: number;
trafficSplit: number;
durationDays: number;
}): Promise<PricingABTest>;
fetchABTests(productId?: string): Promise<PricingABTest[]>;
}
const STRATEGY_NAMES: Record<PricingStrategy, string> = {
GAME_THEORY: '博弈定价',
COMPETITION: '竞争定价',
DEMAND_BASED: '需求定价',
COST_PLUS: '成本加成',
DYNAMIC: '动态定价',
};
const PLATFORM_NAMES: Record<string, string> = {
AMAZON: '亚马逊',
EBAY: 'eBay',
SHOPEE: 'Shopee',
TIKTOK: 'TikTok',
TEMU: 'Temu',
};
class MockDynamicPricingDataSource implements IDynamicPricingDataSource {
private decisions: PricingDecision[] = [];
private configs: Map<string, PricingConfig> = new Map();
private abTests: PricingABTest[] = [];
constructor() {
this.initMockData();
}
private initMockData() {
const products = [
{ id: 'prod-001', name: '无线蓝牙耳机' },
{ id: 'prod-002', name: '智能手表' },
{ id: 'prod-003', name: '便携充电宝' },
{ id: 'prod-004', name: '手机壳' },
{ id: 'prod-005', name: '数据线' },
];
const strategies: PricingStrategy[] = ['GAME_THEORY', 'COMPETITION', 'DEMAND_BASED', 'DYNAMIC'];
for (let i = 0; i < 10; i++) {
const product = products[i % products.length];
const strategy = strategies[i % strategies.length];
const currentPrice = 50 + Math.random() * 100;
const changePercent = (Math.random() - 0.5) * 20;
const suggestedPrice = currentPrice * (1 + changePercent / 100);
this.decisions.push({
id: `PD-${i + 1}`,
tenant_id: 'tenant-001',
shop_id: 'shop-001',
product_id: product.id,
product_name: product.name,
current_price: currentPrice,
suggested_price: suggestedPrice,
price_change: suggestedPrice - currentPrice,
change_percent: changePercent,
adjustment_direction: changePercent > 1 ? 'INCREASE' : changePercent < -1 ? 'DECREASE' : 'MAINTAIN',
strategy_used: strategy,
decision_factors: {
competitor_prices: Array.from({ length: 5 }, () => currentPrice * (0.8 + Math.random() * 0.4)),
market_avg_price: currentPrice * (0.95 + Math.random() * 0.1),
demand_level: Math.random() > 0.6 ? 'HIGH' : Math.random() > 0.3 ? 'MEDIUM' : 'LOW',
inventory_level: Math.floor(Math.random() * 200),
sales_velocity: Math.random() * 10,
profit_margin: 15 + Math.random() * 20,
},
predicted_impact: {
revenue_change: (Math.random() - 0.3) * 500,
profit_change: (Math.random() - 0.3) * 100,
sales_change: (Math.random() - 0.5) * 20,
confidence: 0.6 + Math.random() * 0.35,
},
status: i < 3 ? 'PENDING' : i < 6 ? 'EXECUTED' : 'REJECTED',
created_at: new Date(Date.now() - i * 3600000).toISOString(),
expires_at: new Date(Date.now() + (24 - i) * 3600000).toISOString(),
});
}
}
async generatePricingDecision(productId: string, strategy?: PricingStrategy): Promise<PricingDecision> {
await this.delay(1000);
const currentPrice = 50 + Math.random() * 100;
const changePercent = (Math.random() - 0.5) * 15;
const suggestedPrice = currentPrice * (1 + changePercent / 100);
const decision: PricingDecision = {
id: `PD-${Date.now()}`,
tenant_id: 'tenant-001',
shop_id: 'shop-001',
product_id: productId,
product_name: '商品-' + productId,
current_price: currentPrice,
suggested_price: suggestedPrice,
price_change: suggestedPrice - currentPrice,
change_percent: changePercent,
adjustment_direction: changePercent > 1 ? 'INCREASE' : changePercent < -1 ? 'DECREASE' : 'MAINTAIN',
strategy_used: strategy || 'DYNAMIC',
decision_factors: {
competitor_prices: Array.from({ length: 5 }, () => currentPrice * (0.8 + Math.random() * 0.4)),
market_avg_price: currentPrice * (0.95 + Math.random() * 0.1),
demand_level: Math.random() > 0.5 ? 'HIGH' : 'MEDIUM',
inventory_level: Math.floor(Math.random() * 200),
sales_velocity: Math.random() * 10,
profit_margin: 15 + Math.random() * 20,
},
predicted_impact: {
revenue_change: (Math.random() - 0.3) * 500,
profit_change: (Math.random() - 0.3) * 100,
sales_change: (Math.random() - 0.5) * 20,
confidence: 0.7 + Math.random() * 0.25,
},
status: 'PENDING',
created_at: new Date().toISOString(),
expires_at: new Date(Date.now() + 24 * 3600000).toISOString(),
};
this.decisions.unshift(decision);
return decision;
}
async fetchPendingDecisions(limit: number = 20): Promise<PricingDecision[]> {
await this.delay(300);
return this.decisions.filter(d => d.status === 'PENDING').slice(0, limit);
}
async executePricingDecision(decisionId: string): Promise<{ success: boolean; message: string }> {
await this.delay(500);
const decision = this.decisions.find(d => d.id === decisionId);
if (!decision) {
throw new Error('决策不存在');
}
decision.status = 'EXECUTED';
decision.executed_at = new Date().toISOString();
decision.execution_result = { success: true, actual_price: decision.suggested_price };
return { success: true, message: '价格调整成功' };
}
async rejectPricingDecision(decisionId: string, reason: string): Promise<{ success: boolean }> {
await this.delay(300);
const decision = this.decisions.find(d => d.id === decisionId);
if (!decision) {
throw new Error('决策不存在');
}
decision.status = 'REJECTED';
decision.review_notes = reason;
decision.reviewed_at = new Date().toISOString();
return { success: true };
}
async fetchPricingConfig(productId: string): Promise<PricingConfig | null> {
await this.delay(200);
if (this.configs.has(productId)) {
return this.configs.get(productId) || null;
}
const config: PricingConfig = {
id: `PC-${productId}`,
tenant_id: 'tenant-001',
shop_id: 'shop-001',
product_id: productId,
base_price: 100,
min_price: 70,
max_price: 150,
cost_price: 50,
strategy: 'DYNAMIC',
strategy_params: {
game_theory: { competitor_weight: 0.3, market_share_target: 0.2, profit_priority: 0.7 },
competition: { price_position: 'AVG', max_diff_percent: 10, follow_leader: false },
demand_based: { elasticity_threshold: 1.5, demand_sensitivity: 0.5, seasonal_factor: 1 },
},
auto_adjust: false,
adjust_frequency: 'DAILY',
max_adjust_percent: 10,
cooldown_hours: 24,
min_profit_margin: 15,
min_profit_amount: 0,
enabled: true,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
};
this.configs.set(productId, config);
return config;
}
async updatePricingConfig(productId: string, updates: Partial<PricingConfig>): Promise<PricingConfig> {
await this.delay(300);
const config = await this.fetchPricingConfig(productId);
const updated = { ...config!, ...updates, updated_at: new Date().toISOString() };
this.configs.set(productId, updated);
return updated;
}
async fetchCompetitorPrices(productId: string): Promise<CompetitorPriceRecord[]> {
await this.delay(400);
const platforms = ['AMAZON', 'EBAY', 'SHOPEE', 'TIKTOK', 'TEMU'];
return platforms.map((platform, index) => ({
id: `CP-${productId}-${index}`,
product_id: productId,
competitor_shop_id: `COMP-${platform}-${index}`,
competitor_name: `${PLATFORM_NAMES[platform]}店铺${index + 1}`,
competitor_platform: platform,
price: 80 + Math.random() * 40,
shipping_cost: Math.random() * 10,
total_price: 85 + Math.random() * 45,
stock_status: Math.random() > 0.2 ? 'IN_STOCK' : Math.random() > 0.5 ? 'LOW_STOCK' : 'OUT_OF_STOCK',
rating: 3.5 + Math.random() * 1.5,
review_count: Math.floor(Math.random() * 500),
collected_at: new Date(Date.now() - Math.random() * 3600000).toISOString(),
}));
}
async analyzeCompetitorPrices(productId: string, ourPrice: number): Promise<CompetitorAnalysis> {
await this.delay(500);
const prices = await this.fetchCompetitorPrices(productId);
const allPrices = prices.map(p => p.total_price);
const minPrice = Math.min(...allPrices);
const maxPrice = Math.max(...allPrices);
const avgPrice = allPrices.reduce((a, b) => a + b, 0) / allPrices.length;
return {
product_id: productId,
analysis_date: new Date().toISOString(),
price_stats: {
min_price: minPrice,
max_price: maxPrice,
avg_price: avgPrice,
median_price: allPrices.sort((a, b) => a - b)[Math.floor(allPrices.length / 2)],
our_price: ourPrice,
our_rank: [...allPrices, ourPrice].sort((a, b) => a - b).indexOf(ourPrice) + 1,
total_competitors: prices.length,
},
competitor_rankings: prices.map((p, i) => ({
shop_id: p.competitor_shop_id,
shop_name: p.competitor_name,
price: p.total_price,
rank: i + 1,
price_diff: p.total_price - ourPrice,
rating: p.rating,
review_count: p.review_count,
})),
market_trend: {
price_trend: Math.random() > 0.6 ? 'RISING' : Math.random() > 0.3 ? 'STABLE' : 'FALLING',
competition_level: prices.length > 4 ? 'HIGH' : prices.length > 2 ? 'MEDIUM' : 'LOW',
recommended_action: ourPrice > avgPrice * 1.1 ? 'DECREASE' : ourPrice < avgPrice * 0.9 ? 'INCREASE' : 'MAINTAIN',
},
alerts: [
{ type: 'PRICE_DROP', severity: 'HIGH', message: '发现竞争对手价格低于我们15%', competitor_shop_id: prices[0].competitor_shop_id },
{ type: 'PROMOTION', severity: 'MEDIUM', message: '2个竞争对手正在进行促销活动' },
],
};
}
async createABTest(params: {
productId: string;
testName: string;
controlPrice: number;
variantPrice: number;
trafficSplit: number;
durationDays: number;
}): Promise<PricingABTest> {
await this.delay(500);
const test: PricingABTest = {
id: `ABT-${Date.now()}`,
tenant_id: 'tenant-001',
product_id: params.productId,
test_name: params.testName,
control_price: params.controlPrice,
variant_price: params.variantPrice,
traffic_split: params.trafficSplit,
start_date: new Date().toISOString(),
end_date: new Date(Date.now() + params.durationDays * 86400000).toISOString(),
status: 'RUNNING',
created_at: new Date().toISOString(),
};
this.abTests.push(test);
return test;
}
async fetchABTests(productId?: string): Promise<PricingABTest[]> {
await this.delay(300);
let tests = this.abTests;
if (productId) {
tests = tests.filter(t => t.product_id === productId);
}
return tests;
}
private delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
class ApiDynamicPricingDataSource implements IDynamicPricingDataSource {
private baseUrl = '/api/pricing';
async generatePricingDecision(productId: string, strategy?: PricingStrategy): Promise<PricingDecision> {
const response = await fetch(`${this.baseUrl}/decisions/generate`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ product_id: productId, strategy }),
});
if (!response.ok) throw new Error('Failed to generate pricing decision');
const data = await response.json();
return data.data;
}
async fetchPendingDecisions(limit?: number): Promise<PricingDecision[]> {
const params = new URLSearchParams({ limit: (limit || 20).toString() });
const response = await fetch(`${this.baseUrl}/decisions/pending?${params}`);
if (!response.ok) throw new Error('Failed to fetch pending decisions');
const data = await response.json();
return data.data;
}
async executePricingDecision(decisionId: string): Promise<{ success: boolean; message: string }> {
const response = await fetch(`${this.baseUrl}/decisions/${decisionId}/execute`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
});
if (!response.ok) throw new Error('Failed to execute pricing decision');
const data = await response.json();
return data;
}
async rejectPricingDecision(decisionId: string, reason: string): Promise<{ success: boolean }> {
const response = await fetch(`${this.baseUrl}/decisions/${decisionId}/reject`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ reason }),
});
if (!response.ok) throw new Error('Failed to reject pricing decision');
const data = await response.json();
return data;
}
async fetchPricingConfig(productId: string): Promise<PricingConfig | null> {
const response = await fetch(`${this.baseUrl}/config/${productId}`);
if (!response.ok) return null;
const data = await response.json();
return data.data;
}
async updatePricingConfig(productId: string, config: Partial<PricingConfig>): Promise<PricingConfig> {
const response = await fetch(`${this.baseUrl}/config/${productId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(config),
});
if (!response.ok) throw new Error('Failed to update pricing config');
const data = await response.json();
return data.data;
}
async fetchCompetitorPrices(productId: string): Promise<CompetitorPriceRecord[]> {
const response = await fetch(`${this.baseUrl}/competitors/${productId}`);
if (!response.ok) throw new Error('Failed to fetch competitor prices');
const data = await response.json();
return data.data;
}
async analyzeCompetitorPrices(productId: string, ourPrice: number): Promise<CompetitorAnalysis> {
const response = await fetch(`${this.baseUrl}/competitors/${productId}/analyze?our_price=${ourPrice}`);
if (!response.ok) throw new Error('Failed to analyze competitor prices');
const data = await response.json();
return data.data;
}
async createABTest(params: {
productId: string;
testName: string;
controlPrice: number;
variantPrice: number;
trafficSplit: number;
durationDays: number;
}): Promise<PricingABTest> {
const response = await fetch(`${this.baseUrl}/ab-tests`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(params),
});
if (!response.ok) throw new Error('Failed to create A/B test');
const data = await response.json();
return data.data;
}
async fetchABTests(productId?: string): Promise<PricingABTest[]> {
const params = productId ? `?product_id=${productId}` : '';
const response = await fetch(`${this.baseUrl}/ab-tests${params}`);
if (!response.ok) throw new Error('Failed to fetch A/B tests');
const data = await response.json();
return data.data;
}
}
const useMock = process.env.REACT_APP_USE_MOCK === 'true';
export const dynamicPricingDataSource: IDynamicPricingDataSource = useMock
? new MockDynamicPricingDataSource()
: new ApiDynamicPricingDataSource();
export { STRATEGY_NAMES, PLATFORM_NAMES };

View File

@@ -4,6 +4,8 @@
* 仅在USE_MOCK=true时启用
*/
import { BaseDataSource, BaseMockDataSource, DataSourceFactory } from './dataSourceFactory';
export interface LeaderboardEntry {
rank: number;
tenant_id: string;
@@ -36,7 +38,31 @@ export interface ILeaderboardDataSource {
fetchMyRank(period: 'DAILY' | 'WEEKLY' | 'MONTHLY' | 'ALL_TIME'): Promise<MyRank>;
}
class MockLeaderboardDataSource implements ILeaderboardDataSource {
class ApiLeaderboardDataSource extends BaseDataSource<any, any> implements ILeaderboardDataSource {
constructor() {
super('/api/leaderboard');
}
async fetchLeaderboard(period: 'DAILY' | 'WEEKLY' | 'MONTHLY' | 'ALL_TIME'): Promise<LeaderboardData> {
const response = await fetch(`${this.baseUrl}?period=${period}`);
if (!response.ok) throw new Error('Failed to fetch leaderboard');
return response.json();
}
async fetchMyRank(period: 'DAILY' | 'WEEKLY' | 'MONTHLY' | 'ALL_TIME'): Promise<MyRank> {
const response = await fetch(`${this.baseUrl}/my-rank?period=${period}`);
if (!response.ok) throw new Error('Failed to fetch my rank');
return response.json();
}
}
class MockLeaderboardDataSource extends BaseMockDataSource<any, any> implements ILeaderboardDataSource {
/** Mock数据源名称 */
readonly __MOCK_NAME__ = 'MockLeaderboardDataSource';
/** Mock数据 */
protected mockData: any[] = [];
private generateRankings(count: number, type: 'revenue' | 'roi' | 'growth'): LeaderboardEntry[] {
return Array.from({ length: count }, (_, i) => ({
rank: i + 1,
@@ -54,7 +80,7 @@ class MockLeaderboardDataSource implements ILeaderboardDataSource {
}
async fetchLeaderboard(period: 'DAILY' | 'WEEKLY' | 'MONTHLY' | 'ALL_TIME'): Promise<LeaderboardData> {
await new Promise(resolve => setTimeout(resolve, 500));
await this.delay(500);
return {
revenue: { rankings: this.generateRankings(10, 'revenue'), total: 100 },
roi: { rankings: this.generateRankings(10, 'roi'), total: 100 },
@@ -65,7 +91,7 @@ class MockLeaderboardDataSource implements ILeaderboardDataSource {
}
async fetchMyRank(period: 'DAILY' | 'WEEKLY' | 'MONTHLY' | 'ALL_TIME'): Promise<MyRank> {
await new Promise(resolve => setTimeout(resolve, 300));
await this.delay(300);
return {
revenue: { rank: 15, percentile: 85 },
roi: { rank: 8, percentile: 92 },
@@ -75,23 +101,17 @@ class MockLeaderboardDataSource implements ILeaderboardDataSource {
}
}
class ApiLeaderboardDataSource implements ILeaderboardDataSource {
private baseUrl = '/api/leaderboard';
export const leaderboardDataSource = DataSourceFactory.createWithMethods<
any,
any,
ILeaderboardDataSource
>({
apiDataSource: ApiLeaderboardDataSource,
mockDataSource: MockLeaderboardDataSource,
});
async fetchLeaderboard(period: 'DAILY' | 'WEEKLY' | 'MONTHLY' | 'ALL_TIME'): Promise<LeaderboardData> {
const response = await fetch(`${this.baseUrl}?period=${period}`);
if (!response.ok) throw new Error('Failed to fetch leaderboard');
return response.json();
}
async fetchMyRank(period: 'DAILY' | 'WEEKLY' | 'MONTHLY' | 'ALL_TIME'): Promise<MyRank> {
const response = await fetch(`${this.baseUrl}/my-rank?period=${period}`);
if (!response.ok) throw new Error('Failed to fetch my rank');
return response.json();
}
}
const useMock = process.env.REACT_APP_USE_MOCK === 'true';
export const leaderboardDataSource: ILeaderboardDataSource = useMock
? new MockLeaderboardDataSource()
: new ApiLeaderboardDataSource();
/**
* Mock状态标记
* 用于调试和开发环境识别
*/
export { __MOCK__, __DATA_SOURCE_TYPE__ } from './dataSourceFactory';

View File

@@ -0,0 +1,223 @@
/**
* [MOCK] 退货管理数据源
* AI注意: 这是Mock实现不是真实业务逻辑
* 仅在USE_MOCK=true时启用
*/
export interface SKUData {
id: string;
tenant_id: string;
shop_id: string;
sku_id: string;
sku_name: string;
product_name: string;
return_rate: number;
return_count: number;
sales_count: number;
return_reasons: { reason: string; count: number }[];
status: 'NORMAL' | 'WARNING' | 'CRITICAL';
createdAt: string;
updatedAt: string;
}
export interface ReturnData {
id: string;
tenant_id: string;
shop_id: string;
sku_id: string;
sku_name: string;
product_name: string;
order_id: string;
return_reason: string;
return_type: 'REFUND' | 'EXCHANGE' | 'RETURN';
status: 'PENDING' | 'APPROVED' | 'PROCESSING' | 'COMPLETED' | 'REJECTED';
amount: number;
created_at: string;
updated_at: string;
}
export interface ReturnTrend {
date: string;
returnCount: number;
returnRate: number;
refundAmount: number;
}
export interface IReturnDataSource {
fetchSKUData(params?: { status?: string; shop_id?: string }): Promise<SKUData[]>;
updateSKUStatus(id: string, status: 'NORMAL' | 'WARNING' | 'CRITICAL'): Promise<SKUData>;
fetchReturns(params?: { status?: string; shop_id?: string; sku_id?: string }): Promise<ReturnData[]>;
processReturn(id: string, action: 'APPROVE' | 'REJECT'): Promise<ReturnData>;
fetchReturnTrends(params?: { start_date?: string; end_date?: string }): Promise<ReturnTrend[]>;
}
class MockReturnDataSource implements IReturnDataSource {
private skuData: SKUData[] = [
{
id: '1',
tenant_id: 'tenant_001',
shop_id: 'shop_001',
sku_id: 'SKU-001',
sku_name: 'Product A - Blue',
product_name: 'Product A',
return_rate: 15.5,
return_count: 155,
sales_count: 1000,
return_reasons: [
{ reason: 'Wrong size', count: 50 },
{ reason: 'Defective', count: 40 },
{ reason: 'Not as described', count: 35 },
{ reason: 'Changed mind', count: 30 },
],
status: 'WARNING',
createdAt: '2026-03-01',
updatedAt: '2026-03-15',
},
{
id: '2',
tenant_id: 'tenant_001',
shop_id: 'shop_001',
sku_id: 'SKU-002',
sku_name: 'Product B - Red',
product_name: 'Product B',
return_rate: 5.2,
return_count: 52,
sales_count: 1000,
return_reasons: [
{ reason: 'Changed mind', count: 30 },
{ reason: 'Defective', count: 22 },
],
status: 'NORMAL',
createdAt: '2026-03-01',
updatedAt: '2026-03-15',
},
];
private returns: ReturnData[] = [
{
id: '1',
tenant_id: 'tenant_001',
shop_id: 'shop_001',
sku_id: 'SKU-001',
sku_name: 'Product A - Blue',
product_name: 'Product A',
order_id: 'ORD-2026-001',
return_reason: 'Wrong size',
return_type: 'EXCHANGE',
status: 'PENDING',
amount: 99.99,
created_at: '2026-03-15',
updated_at: '2026-03-15',
},
];
private trends: ReturnTrend[] = [
{ date: '2026-03-10', returnCount: 12, returnRate: 3.5, refundAmount: 1200 },
{ date: '2026-03-11', returnCount: 15, returnRate: 4.2, refundAmount: 1500 },
{ date: '2026-03-12', returnCount: 10, returnRate: 2.8, refundAmount: 1000 },
{ date: '2026-03-13', returnCount: 18, returnRate: 5.1, refundAmount: 1800 },
{ date: '2026-03-14', returnCount: 14, returnRate: 3.9, refundAmount: 1400 },
{ date: '2026-03-15', returnCount: 20, returnRate: 5.6, refundAmount: 2000 },
];
async fetchSKUData(params?: { status?: string; shop_id?: string }): Promise<SKUData[]> {
await new Promise(resolve => setTimeout(resolve, 300));
let result = [...this.skuData];
if (params?.status) {
result = result.filter(s => s.status === params.status);
}
if (params?.shop_id) {
result = result.filter(s => s.shop_id === params.shop_id);
}
return result;
}
async updateSKUStatus(id: string, status: 'NORMAL' | 'WARNING' | 'CRITICAL'): Promise<SKUData> {
await new Promise(resolve => setTimeout(resolve, 300));
const index = this.skuData.findIndex(s => s.id === id);
if (index === -1) throw new Error('SKU not found');
this.skuData[index] = { ...this.skuData[index], status, updatedAt: new Date().toISOString().split('T')[0] };
return this.skuData[index];
}
async fetchReturns(params?: { status?: string; shop_id?: string; sku_id?: string }): Promise<ReturnData[]> {
await new Promise(resolve => setTimeout(resolve, 300));
let result = [...this.returns];
if (params?.status) {
result = result.filter(r => r.status === params.status);
}
if (params?.shop_id) {
result = result.filter(r => r.shop_id === params.shop_id);
}
if (params?.sku_id) {
result = result.filter(r => r.sku_id === params.sku_id);
}
return result;
}
async processReturn(id: string, action: 'APPROVE' | 'REJECT'): Promise<ReturnData> {
await new Promise(resolve => setTimeout(resolve, 300));
const index = this.returns.findIndex(r => r.id === id);
if (index === -1) throw new Error('Return not found');
this.returns[index] = {
...this.returns[index],
status: action === 'APPROVE' ? 'APPROVED' : 'REJECTED',
updated_at: new Date().toISOString().split('T')[0],
};
return this.returns[index];
}
async fetchReturnTrends(params?: { start_date?: string; end_date?: string }): Promise<ReturnTrend[]> {
await new Promise(resolve => setTimeout(resolve, 300));
return [...this.trends];
}
}
class ApiReturnDataSource implements IReturnDataSource {
private baseUrl = '/api/returns';
async fetchSKUData(params?: { status?: string; shop_id?: string }): Promise<SKUData[]> {
const response = await fetch(`${this.baseUrl}/sku?${new URLSearchParams(params as any)}`);
if (!response.ok) throw new Error('Failed to fetch SKU data');
return response.json();
}
async updateSKUStatus(id: string, status: 'NORMAL' | 'WARNING' | 'CRITICAL'): Promise<SKUData> {
const response = await fetch(`${this.baseUrl}/sku/${id}/status`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ status }),
});
if (!response.ok) throw new Error('Failed to update SKU status');
return response.json();
}
async fetchReturns(params?: { status?: string; shop_id?: string; sku_id?: string }): Promise<ReturnData[]> {
const response = await fetch(`${this.baseUrl}?${new URLSearchParams(params as any)}`);
if (!response.ok) throw new Error('Failed to fetch returns');
return response.json();
}
async processReturn(id: string, action: 'APPROVE' | 'REJECT'): Promise<ReturnData> {
const response = await fetch(`${this.baseUrl}/${id}/process`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action }),
});
if (!response.ok) throw new Error('Failed to process return');
return response.json();
}
async fetchReturnTrends(params?: { start_date?: string; end_date?: string }): Promise<ReturnTrend[]> {
const response = await fetch(`${this.baseUrl}/trends?${new URLSearchParams(params as any)}`);
if (!response.ok) throw new Error('Failed to fetch return trends');
return response.json();
}
}
const useMock = process.env.REACT_APP_USE_MOCK === 'true';
export const returnDataSource: IReturnDataSource = useMock
? new MockReturnDataSource()
: new ApiReturnDataSource();

View File

@@ -0,0 +1,394 @@
/**
* [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 };

View File

@@ -0,0 +1,237 @@
/**
* [MOCK] 用户资产数据源
* AI注意: 这是Mock实现不是真实业务逻辑
* 仅在USE_MOCK=true时启用
*/
export type MemberLevel = 'BRONZE' | 'SILVER' | 'GOLD' | 'PLATINUM' | 'DIAMOND';
export interface UserAsset {
id: string;
tenantId: string;
userId: string;
userName: string;
email: string;
memberLevel: MemberLevel;
points: number;
totalSpent: number;
availableBalance: number;
frozenBalance: number;
totalOrders: number;
joinDate: string;
lastActiveDate: string;
status: 'ACTIVE' | 'INACTIVE' | 'SUSPENDED';
}
export interface MemberLevelConfig {
id: string;
level: MemberLevel;
name: string;
minSpent: number;
discount: number;
pointsMultiplier: number;
benefits: string[];
color: string;
icon: string;
}
export interface PointsRecord {
id: string;
userId: string;
userName: string;
type: 'EARN' | 'REDEEM' | 'EXPIRE' | 'ADJUST';
amount: number;
balance: number;
source: string;
description: string;
createdAt: string;
}
export interface IUserAssetDataSource {
fetchUserAssets(params?: { memberLevel?: string; status?: string; search?: string }): Promise<UserAsset[]>;
updateUserAsset(id: string, data: Partial<UserAsset>): Promise<UserAsset>;
fetchMemberLevelConfigs(): Promise<MemberLevelConfig[]>;
updateMemberLevelConfig(id: string, data: Partial<MemberLevelConfig>): Promise<MemberLevelConfig>;
fetchPointsRecords(params?: { userId?: string; type?: string }): Promise<PointsRecord[]>;
adjustPoints(userId: string, amount: number, reason: string): Promise<PointsRecord>;
}
class MockUserAssetDataSource implements IUserAssetDataSource {
private userAssets: UserAsset[] = [
{
id: '1',
tenantId: 'tenant_001',
userId: 'user_001',
userName: 'John Doe',
email: 'john@example.com',
memberLevel: 'GOLD',
points: 5000,
totalSpent: 15000,
availableBalance: 500,
frozenBalance: 100,
totalOrders: 25,
joinDate: '2025-01-15',
lastActiveDate: '2026-03-18',
status: 'ACTIVE',
},
{
id: '2',
tenantId: 'tenant_001',
userId: 'user_002',
userName: 'Jane Smith',
email: 'jane@example.com',
memberLevel: 'PLATINUM',
points: 15000,
totalSpent: 50000,
availableBalance: 1200,
frozenBalance: 0,
totalOrders: 85,
joinDate: '2024-06-20',
lastActiveDate: '2026-03-19',
status: 'ACTIVE',
},
];
private memberLevelConfigs: MemberLevelConfig[] = [
{ id: '1', level: 'BRONZE', name: 'Bronze', minSpent: 0, discount: 0, pointsMultiplier: 1, benefits: ['Basic support'], color: '#CD7F32', icon: '🥉' },
{ id: '2', level: 'SILVER', name: 'Silver', minSpent: 1000, discount: 5, pointsMultiplier: 1.2, benefits: ['Priority support', '5% discount'], color: '#C0C0C0', icon: '🥈' },
{ id: '3', level: 'GOLD', name: 'Gold', minSpent: 5000, discount: 10, pointsMultiplier: 1.5, benefits: ['VIP support', '10% discount', 'Free shipping'], color: '#FFD700', icon: '🥇' },
{ id: '4', level: 'PLATINUM', name: 'Platinum', minSpent: 20000, discount: 15, pointsMultiplier: 2, benefits: ['Dedicated support', '15% discount', 'Free express shipping', 'Birthday gift'], color: '#E5E4E2', icon: '💎' },
{ id: '5', level: 'DIAMOND', name: 'Diamond', minSpent: 50000, discount: 20, pointsMultiplier: 3, benefits: ['Personal account manager', '20% discount', 'Exclusive events'], color: '#B9F2FF', icon: '💠' },
];
private pointsRecords: PointsRecord[] = [
{ id: '1', userId: 'user_001', userName: 'John Doe', type: 'EARN', amount: 100, balance: 5000, source: 'Purchase', description: 'Order #ORD-001', createdAt: '2026-03-15' },
{ id: '2', userId: 'user_001', userName: 'John Doe', type: 'REDEEM', amount: -50, balance: 4950, source: 'Redemption', description: 'Coupon redemption', createdAt: '2026-03-16' },
];
async fetchUserAssets(params?: { memberLevel?: string; status?: string; search?: string }): Promise<UserAsset[]> {
await new Promise(resolve => setTimeout(resolve, 300));
let result = [...this.userAssets];
if (params?.memberLevel) {
result = result.filter(u => u.memberLevel === params.memberLevel);
}
if (params?.status) {
result = result.filter(u => u.status === params.status);
}
if (params?.search) {
result = result.filter(u => u.userName.toLowerCase().includes(params.search!.toLowerCase()) || u.email.toLowerCase().includes(params.search!.toLowerCase()));
}
return result;
}
async updateUserAsset(id: string, data: Partial<UserAsset>): Promise<UserAsset> {
await new Promise(resolve => setTimeout(resolve, 300));
const index = this.userAssets.findIndex(u => u.id === id);
if (index === -1) throw new Error('User asset not found');
this.userAssets[index] = { ...this.userAssets[index], ...data };
return this.userAssets[index];
}
async fetchMemberLevelConfigs(): Promise<MemberLevelConfig[]> {
await new Promise(resolve => setTimeout(resolve, 300));
return [...this.memberLevelConfigs];
}
async updateMemberLevelConfig(id: string, data: Partial<MemberLevelConfig>): Promise<MemberLevelConfig> {
await new Promise(resolve => setTimeout(resolve, 300));
const index = this.memberLevelConfigs.findIndex(c => c.id === id);
if (index === -1) throw new Error('Config not found');
this.memberLevelConfigs[index] = { ...this.memberLevelConfigs[index], ...data };
return this.memberLevelConfigs[index];
}
async fetchPointsRecords(params?: { userId?: string; type?: string }): Promise<PointsRecord[]> {
await new Promise(resolve => setTimeout(resolve, 300));
let result = [...this.pointsRecords];
if (params?.userId) {
result = result.filter(r => r.userId === params.userId);
}
if (params?.type) {
result = result.filter(r => r.type === params.type);
}
return result;
}
async adjustPoints(userId: string, amount: number, reason: string): Promise<PointsRecord> {
await new Promise(resolve => setTimeout(resolve, 300));
const user = this.userAssets.find(u => u.userId === userId);
if (!user) throw new Error('User not found');
const newBalance = user.points + amount;
const record: PointsRecord = {
id: `${Date.now()}`,
userId,
userName: user.userName,
type: amount > 0 ? 'EARN' : 'REDEEM',
amount,
balance: newBalance,
source: 'Manual Adjustment',
description: reason,
createdAt: new Date().toISOString().split('T')[0],
};
user.points = newBalance;
this.pointsRecords.push(record);
return record;
}
}
class ApiUserAssetDataSource implements IUserAssetDataSource {
private baseUrl = '/api/user-assets';
async fetchUserAssets(params?: { memberLevel?: string; status?: string; search?: string }): Promise<UserAsset[]> {
const response = await fetch(`${this.baseUrl}?${new URLSearchParams(params as any)}`);
if (!response.ok) throw new Error('Failed to fetch user assets');
return response.json();
}
async updateUserAsset(id: string, data: Partial<UserAsset>): Promise<UserAsset> {
const response = await fetch(`${this.baseUrl}/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
if (!response.ok) throw new Error('Failed to update user asset');
return response.json();
}
async fetchMemberLevelConfigs(): Promise<MemberLevelConfig[]> {
const response = await fetch(`${this.baseUrl}/member-levels`);
if (!response.ok) throw new Error('Failed to fetch member level configs');
return response.json();
}
async updateMemberLevelConfig(id: string, data: Partial<MemberLevelConfig>): Promise<MemberLevelConfig> {
const response = await fetch(`${this.baseUrl}/member-levels/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
if (!response.ok) throw new Error('Failed to update member level config');
return response.json();
}
async fetchPointsRecords(params?: { userId?: string; type?: string }): Promise<PointsRecord[]> {
const response = await fetch(`${this.baseUrl}/points?${new URLSearchParams(params as any)}`);
if (!response.ok) throw new Error('Failed to fetch points records');
return response.json();
}
async adjustPoints(userId: string, amount: number, reason: string): Promise<PointsRecord> {
const response = await fetch(`${this.baseUrl}/points/adjust`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userId, amount, reason }),
});
if (!response.ok) throw new Error('Failed to adjust points');
return response.json();
}
}
const useMock = process.env.REACT_APP_USE_MOCK === 'true';
export const userAssetDataSource: IUserAssetDataSource = useMock
? new MockUserAssetDataSource()
: new ApiUserAssetDataSource();