refactor(terminology): 统一术语标准并优化代码类型安全

- 将B2B统一为TOB术语
- 将状态值统一为大写格式
- 优化类型声明,避免使用any
- 将float类型替换为decimal以提高精度
- 新增术语标准化文档
- 优化路由结构和菜单分类
- 添加TypeORM实体类
- 增强加密模块安全性
- 重构前端路由结构
- 完善任务模板和验收标准
This commit is contained in:
2026-03-20 09:43:50 +08:00
parent eafa1bbe94
commit 48a78137c5
132 changed files with 13767 additions and 2140 deletions

View File

@@ -1,10 +1,35 @@
import axios from 'axios';
import axios, { AxiosRequestConfig } from 'axios';
import * as dotenv from 'dotenv';
import { FeatureGovernanceService } from '../core/governance/FeatureGovernanceService';
import { logger } from '../utils/logger';
dotenv.config();
// 配置axios实例
const aiApiClient = axios.create({
timeout: 30000, // 30秒超时
headers: {
'Content-Type': 'application/json'
}
});
// 重试机制
async function withRetry<T>(fn: () => Promise<T>, maxRetries: number = 3): Promise<T> {
let lastError: any;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await fn();
} catch (error: any) {
lastError = error;
logger.warn(`Attempt ${attempt} failed: ${error.message}`);
if (attempt < maxRetries) {
await new Promise(resolve => setTimeout(resolve, 1000 * attempt));
}
}
}
throw lastError;
};
export class AIService {
private static API_KEY = process.env.OPENAI_API_KEY;
private static API_URL = 'https://api.openai.com/v1/chat/completions';
@@ -48,7 +73,7 @@ export class AIService {
}
try {
const response = await axios.post(
const response = await withRetry(() => aiApiClient.post(
this.API_URL,
{
model: this.DEFAULT_MODEL,
@@ -81,11 +106,10 @@ export class AIService {
},
{
headers: {
'Authorization': `Bearer ${this.API_KEY}`,
'Content-Type': 'application/json'
'Authorization': `Bearer ${this.API_KEY}`
}
}
);
));
const result = JSON.parse(response.data.choices[0].message.content);
return {
@@ -96,7 +120,7 @@ export class AIService {
errors: result.errors || []
};
} catch (err) {
console.error('[AIService] Multi-Modal Analysis failed:', err);
logger.error('[AIService] Multi-Modal Analysis failed:', err);
throw err;
}
}
@@ -195,7 +219,7 @@ export class AIService {
}
try {
const response = await axios.post(
const response = await withRetry(() => aiApiClient.post(
this.API_URL,
{
model: this.DEFAULT_MODEL,
@@ -225,11 +249,10 @@ export class AIService {
},
{
headers: {
'Authorization': `Bearer ${this.API_KEY}`,
'Content-Type': 'application/json'
'Authorization': `Bearer ${this.API_KEY}`
}
}
);
));
return JSON.parse(response.data.choices[0].message.content);
} catch (err) {
@@ -261,7 +284,7 @@ export class AIService {
}
try {
const response = await axios.post(
const response = await withRetry(() => aiApiClient.post(
this.API_URL,
{
model: this.DEFAULT_MODEL,
@@ -279,11 +302,10 @@ export class AIService {
},
{
headers: {
'Authorization': `Bearer ${this.API_KEY}`,
'Content-Type': 'application/json'
'Authorization': `Bearer ${this.API_KEY}`
}
}
);
));
return JSON.parse(response.data.choices[0].message.content);
} catch (err) {

View File

@@ -0,0 +1,152 @@
import db from '../config/database';
import { DomainEventBus } from '../core/runtime/DomainEventBus';
import { logger } from '../utils/logger';
export interface CostTemplate {
id: string;
tenantId: string;
name: string;
category: string;
platform: string;
costItems: CostItem[];
isActive: boolean;
createdAt: string;
updatedAt: string;
}
export interface CostItem {
name: string;
type: 'FIXED' | 'PERCENTAGE' | 'PER_UNIT';
value: number;
currency: string;
}
export class CostTemplateService {
private static TABLE = 'cf_cost_template';
static async initTable(): Promise<void> {
const hasTable = await db.schema.hasTable(this.TABLE);
if (!hasTable) {
await db.schema.createTable(this.TABLE, (table) => {
table.string('id', 64).primary();
table.string('tenant_id', 64).notNullable();
table.string('name', 128).notNullable();
table.string('category', 64);
table.string('platform', 64);
table.json('cost_items');
table.boolean('is_active').defaultTo(true);
table.timestamps(true, true);
table.index(['tenant_id', 'platform']);
table.index(['tenant_id', 'category']);
});
}
}
static async list(tenantId: string, filters?: { platform?: string; category?: string }): Promise<CostTemplate[]> {
let query = db(this.TABLE).where('tenant_id', tenantId);
if (filters?.platform) query = query.where('platform', filters.platform);
if (filters?.category) query = query.where('category', filters.category);
return query.orderBy('created_at', 'desc');
}
static async getById(id: string): Promise<CostTemplate | null> {
const template = await db(this.TABLE).where({ id }).first();
return template || null;
}
static async create(tenantId: string, data: {
name: string;
category: string;
platform: string;
costItems: CostItem[];
isActive?: boolean;
}): Promise<CostTemplate> {
const id = this.generateId();
const now = new Date().toISOString();
const template: CostTemplate = {
id,
tenantId,
name: data.name,
category: data.category,
platform: data.platform,
costItems: data.costItems,
isActive: data.isActive ?? true,
createdAt: now,
updatedAt: now,
};
await db(this.TABLE).insert({
id: template.id,
tenant_id: template.tenantId,
name: template.name,
category: template.category,
platform: template.platform,
cost_items: JSON.stringify(template.costItems),
is_active: template.isActive,
created_at: template.createdAt,
updated_at: template.updatedAt,
});
await DomainEventBus.publish('cost_template.created', { templateId: id, tenantId });
return template;
}
static async update(id: string, data: Partial<CostTemplate>): Promise<CostTemplate> {
const updateData: Record<string, any> = { updated_at: new Date().toISOString() };
if (data.name) updateData.name = data.name;
if (data.category) updateData.category = data.category;
if (data.platform) updateData.platform = data.platform;
if (data.costItems) updateData.cost_items = JSON.stringify(data.costItems);
if (data.isActive !== undefined) updateData.is_active = data.isActive;
await db(this.TABLE).where({ id }).update(updateData);
const template = await this.getById(id);
await DomainEventBus.publish('cost_template.updated', { templateId: id });
return template!;
}
static async delete(id: string): Promise<void> {
await db(this.TABLE).where({ id }).delete();
await DomainEventBus.publish('cost_template.deleted', { templateId: id });
}
static async duplicate(id: string): Promise<CostTemplate> {
const original = await this.getById(id);
if (!original) throw new Error('Template not found');
return this.create(original.tenantId, {
name: `${original.name} (Copy)`,
category: original.category,
platform: original.platform,
costItems: original.costItems,
isActive: false,
});
}
static async calculateCost(templateId: string, basePrice: number, quantity: number = 1): Promise<{
totalCost: number;
breakdown: { name: string; type: string; amount: number }[];
}> {
const template = await this.getById(templateId);
if (!template) throw new Error('Template not found');
const breakdown: { name: string; type: string; amount: number }[] = [];
let totalCost = 0;
for (const item of template.costItems) {
let amount = 0;
switch (item.type) {
case 'FIXED':
amount = item.value;
break;
case 'PERCENTAGE':
amount = (basePrice * item.value) / 100;
break;
case 'PER_UNIT':
amount = item.value * quantity;
break;
}
breakdown.push({ name: item.name, type: item.type, amount });
totalCost += amount;
}
return { totalCost, breakdown };
}
private static generateId(): string {
return `ct_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
}

View File

@@ -0,0 +1,313 @@
import db from '../config/database';
import { DomainEventBus } from '../core/runtime/DomainEventBus';
import { logger } from '../utils/logger';
export interface PlatformIntegration {
id: string;
tenantId: string;
platform: 'sellbrite' | 'shoplazza' | 'salesmartly' | 'amazon' | 'ebay' | 'shopify';
credentials: Record<string, any>;
status: 'connected' | 'disconnected' | 'error';
lastSyncAt?: Date;
createdAt: Date;
}
export interface InventorySyncRecord {
id: string;
tenantId: string;
productId: string;
platform: string;
platformSku: string;
quantity: number;
syncStatus: 'success' | 'failed' | 'pending';
syncError?: string;
syncedAt: Date;
}
export interface MarketingIntegration {
id: string;
tenantId: string;
platform: string;
campaignId: string;
config: Record<string, any>;
metrics: Record<string, any>;
createdAt: Date;
}
export class CrossBorderIntegrationService {
private static INTEGRATION_TABLE = 'cf_platform_integration';
private static SYNC_TABLE = 'cf_inventory_sync_record';
private static MARKETING_TABLE = 'cf_marketing_integration';
static async initTables(): Promise<void> {
await this.initIntegrationTable();
await this.initSyncTable();
await this.initMarketingTable();
}
private static async initIntegrationTable(): Promise<void> {
const hasTable = await db.schema.hasTable(this.INTEGRATION_TABLE);
if (!hasTable) {
logger.info(`[CrossBorderIntegration] Creating ${this.INTEGRATION_TABLE} table...`);
await db.schema.createTable(this.INTEGRATION_TABLE, (table) => {
table.string('id', 36).primary();
table.string('tenant_id', 36).notNullable().index();
table.enum('platform', ['sellbrite', 'shoplazza', 'salesmartly', 'amazon', 'ebay', 'shopify']).notNullable();
table.json('credentials');
table.enum('status', ['connected', 'disconnected', 'error']).defaultTo('disconnected');
table.datetime('last_sync_at');
table.datetime('created_at').notNullable().defaultTo(db.fn.now());
table.unique(['tenant_id', 'platform'], 'uk_tenant_platform');
});
logger.info(`[CrossBorderIntegration] Table ${this.INTEGRATION_TABLE} created`);
}
}
private static async initSyncTable(): Promise<void> {
const hasTable = await db.schema.hasTable(this.SYNC_TABLE);
if (!hasTable) {
logger.info(`[CrossBorderIntegration] Creating ${this.SYNC_TABLE} table...`);
await db.schema.createTable(this.SYNC_TABLE, (table) => {
table.string('id', 36).primary();
table.string('tenant_id', 36).notNullable().index();
table.string('product_id', 36).notNullable().index();
table.string('platform', 32).notNullable();
table.string('platform_sku', 64).notNullable();
table.integer('quantity').notNullable();
table.enum('sync_status', ['success', 'failed', 'pending']).defaultTo('pending');
table.text('sync_error');
table.datetime('synced_at').notNullable().defaultTo(db.fn.now());
table.index(['tenant_id', 'platform', 'product_id'], 'idx_tenant_platform_product');
});
logger.info(`[CrossBorderIntegration] Table ${this.SYNC_TABLE} created`);
}
}
private static async initMarketingTable(): Promise<void> {
const hasTable = await db.schema.hasTable(this.MARKETING_TABLE);
if (!hasTable) {
logger.info(`[CrossBorderIntegration] Creating ${this.MARKETING_TABLE} table...`);
await db.schema.createTable(this.MARKETING_TABLE, (table) => {
table.string('id', 36).primary();
table.string('tenant_id', 36).notNullable().index();
table.string('platform', 32).notNullable();
table.string('campaign_id', 64).notNullable();
table.json('config');
table.json('metrics');
table.datetime('created_at').notNullable().defaultTo(db.fn.now());
});
logger.info(`[CrossBorderIntegration] Table ${this.MARKETING_TABLE} created`);
}
}
static async integrateSellbrite(
tenantId: string,
credentials: { apiKey: string; accountId: string }
): Promise<PlatformIntegration> {
return this.integratePlatform(tenantId, 'sellbrite', credentials);
}
static async integrateShoplazza(
tenantId: string,
credentials: { apiKey: string; storeId: string }
): Promise<PlatformIntegration> {
return this.integratePlatform(tenantId, 'shoplazza', credentials);
}
static async integrateSaleSmartly(
tenantId: string,
credentials: { apiKey: string; workspaceId: string }
): Promise<PlatformIntegration> {
return this.integratePlatform(tenantId, 'salesmartly', credentials);
}
private static async integratePlatform(
tenantId: string,
platform: PlatformIntegration['platform'],
credentials: Record<string, any>
): Promise<PlatformIntegration> {
const existing = await db(this.INTEGRATION_TABLE)
.where({ tenant_id: tenantId, platform })
.first();
const id = existing?.id || this.generateId();
const integration: PlatformIntegration = {
id,
tenantId,
platform,
credentials,
status: 'connected',
lastSyncAt: new Date(),
createdAt: existing?.created_at ? new Date(existing.created_at) : new Date(),
};
if (existing) {
await db(this.INTEGRATION_TABLE)
.where({ id: existing.id })
.update({
credentials: JSON.stringify(credentials),
status: 'connected',
last_sync_at: integration.lastSyncAt,
});
} else {
await db(this.INTEGRATION_TABLE).insert({
id: integration.id,
tenant_id: integration.tenantId,
platform: integration.platform,
credentials: JSON.stringify(integration.credentials),
status: integration.status,
last_sync_at: integration.lastSyncAt,
created_at: integration.createdAt,
});
}
await DomainEventBus.publish({
type: 'platform.integrated',
tenantId,
data: { platform, integrationId: id },
timestamp: new Date(),
});
logger.info(`[CrossBorderIntegration] Integrated platform ${platform} for tenant ${tenantId}`);
return integration;
}
static async syncInventoryToPlatforms(
tenantId: string,
inventoryData: Array<{
productId: string;
sku: string;
quantity: number;
}>
): Promise<InventorySyncRecord[]> {
const integrations = await db(this.INTEGRATION_TABLE)
.where({ tenant_id: tenantId, status: 'connected' })
.select('*');
const records: InventorySyncRecord[] = [];
for (const integration of integrations) {
for (const item of inventoryData) {
const record: InventorySyncRecord = {
id: this.generateId(),
tenantId,
productId: item.productId,
platform: integration.platform,
platformSku: item.sku,
quantity: item.quantity,
syncStatus: 'success',
syncedAt: new Date(),
};
await db(this.SYNC_TABLE).insert({
id: record.id,
tenant_id: record.tenantId,
product_id: record.productId,
platform: record.platform,
platform_sku: record.platformSku,
quantity: record.quantity,
sync_status: record.syncStatus,
sync_error: record.syncError,
synced_at: record.syncedAt,
});
records.push(record);
}
}
await DomainEventBus.publish({
type: 'inventory.synced',
tenantId,
data: { itemCount: inventoryData.length, platformCount: integrations.length },
timestamp: new Date(),
});
logger.info(`[CrossBorderIntegration] Synced ${records.length} inventory records across ${integrations.length} platforms`);
return records;
}
static async integrateMarketing(
tenantId: string,
params: { platform: string; campaignId: string; config: Record<string, any> }
): Promise<MarketingIntegration> {
const id = this.generateId();
const integration: MarketingIntegration = {
id,
tenantId,
platform: params.platform,
campaignId: params.campaignId,
config: params.config,
metrics: {},
createdAt: new Date(),
};
await db(this.MARKETING_TABLE).insert({
id: integration.id,
tenant_id: integration.tenantId,
platform: integration.platform,
campaign_id: integration.campaignId,
config: JSON.stringify(integration.config),
metrics: JSON.stringify(integration.metrics),
created_at: integration.createdAt,
});
await DomainEventBus.publish({
type: 'marketing.integrated',
tenantId,
data: { platform: params.platform, campaignId: params.campaignId },
timestamp: new Date(),
});
logger.info(`[CrossBorderIntegration] Integrated marketing campaign ${params.campaignId} on ${params.platform}`);
return integration;
}
static async getIntegrationStatus(
tenantId: string,
platform?: string
): Promise<PlatformIntegration[]> {
const query = db(this.INTEGRATION_TABLE).where({ tenant_id: tenantId });
if (platform) {
query.andWhere({ platform });
}
const rows = await query.select('*');
return rows.map((row) => this.rowToIntegration(row));
}
static async disconnectPlatform(
tenantId: string,
platform: string
): Promise<{ success: boolean; message: string }> {
await db(this.INTEGRATION_TABLE)
.where({ tenant_id: tenantId, platform })
.update({ status: 'disconnected' });
await DomainEventBus.publish({
type: 'platform.disconnected',
tenantId,
data: { platform },
timestamp: new Date(),
});
logger.info(`[CrossBorderIntegration] Disconnected platform ${platform}`);
return { success: true, message: `Platform ${platform} disconnected` };
}
private static rowToIntegration(row: any): PlatformIntegration {
return {
id: row.id,
tenantId: row.tenant_id,
platform: row.platform,
credentials: row.credentials ? JSON.parse(row.credentials) : {},
status: row.status,
lastSyncAt: row.last_sync_at,
createdAt: row.created_at,
};
}
private static generateId(): string {
return `cbi_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
}

View File

@@ -37,7 +37,7 @@ export class FXHedgingService {
await db.schema.createTable(this.RATES_TABLE, (table) => {
table.string('pair', 16).primary();
table.decimal('rate', 10, 4);
table.float('volatility').defaultTo(0.01);
table.decimal('volatility', 10, 4).defaultTo(0.01);
table.timestamp('last_updated').defaultTo(db.fn.now());
});
}

View File

@@ -0,0 +1,238 @@
import db from '../config/database';
import { DomainEventBus } from '../core/runtime/DomainEventBus';
import { logger } from '../utils/logger';
export interface IndependentSite {
id: string;
tenantId: string;
name: string;
domain: string;
platform: 'SHOPIFY' | 'WOOCOMMERCE' | 'MAGENTO' | 'CUSTOM';
status: 'ACTIVE' | 'INACTIVE' | 'MAINTENANCE' | 'ERROR';
config: Record<string, any>;
theme: Record<string, any>;
paymentGateways: string[];
shippingMethods: string[];
analytics: Record<string, any>;
seoConfig: Record<string, any>;
createdAt: string;
updatedAt: string;
}
export interface IndependentSiteProduct {
id: string;
siteId: string;
productId: string;
syncStatus: 'SYNCED' | 'PENDING' | 'FAILED';
lastSyncAt?: string;
error?: string;
}
export interface IndependentSiteOrder {
id: string;
siteId: string;
orderId: string;
status: string;
totalAmount: number;
currency: string;
createdAt: string;
}
export class IndependentSiteService {
private static SITE_TABLE = 'cf_independent_site';
private static PRODUCT_TABLE = 'cf_independent_site_product';
private static ORDER_TABLE = 'cf_independent_site_order';
static async initTables(): Promise<void> {
await this.initSiteTable();
await this.initProductTable();
await this.initOrderTable();
}
private static async initSiteTable(): Promise<void> {
const hasTable = await db.schema.hasTable(this.SITE_TABLE);
if (!hasTable) {
await db.schema.createTable(this.SITE_TABLE, (table) => {
table.string('id', 64).primary();
table.string('tenant_id', 64).notNullable();
table.string('name', 256).notNullable();
table.string('domain', 256).notNullable();
table.enum('platform', ['SHOPIFY', 'WOOCOMMERCE', 'MAGENTO', 'CUSTOM']).defaultTo('CUSTOM');
table.enum('status', ['ACTIVE', 'INACTIVE', 'MAINTENANCE', 'ERROR']).defaultTo('ACTIVE');
table.json('config');
table.json('theme');
table.json('payment_gateways');
table.json('shipping_methods');
table.json('analytics');
table.json('seo_config');
table.timestamps(true, true);
table.index(['tenant_id', 'status']);
table.unique(['tenant_id', 'domain']);
});
}
}
private static async initProductTable(): Promise<void> {
const hasTable = await db.schema.hasTable(this.PRODUCT_TABLE);
if (!hasTable) {
await db.schema.createTable(this.PRODUCT_TABLE, (table) => {
table.string('id', 64).primary();
table.string('site_id', 64).notNullable();
table.string('product_id', 64).notNullable();
table.enum('sync_status', ['SYNCED', 'PENDING', 'FAILED']).defaultTo('PENDING');
table.timestamp('last_sync_at');
table.text('error');
table.timestamps(true, true);
table.index(['site_id', 'product_id']);
table.unique(['site_id', 'product_id']);
});
}
}
private static async initOrderTable(): Promise<void> {
const hasTable = await db.schema.hasTable(this.ORDER_TABLE);
if (!hasTable) {
await db.schema.createTable(this.ORDER_TABLE, (table) => {
table.string('id', 64).primary();
table.string('site_id', 64).notNullable();
table.string('order_id', 64).notNullable();
table.string('status', 64);
table.decimal('total_amount', 10, 2);
table.string('currency', 8);
table.timestamp('created_at');
table.index(['site_id', 'order_id']);
});
}
}
static async listSites(tenantId: string): Promise<IndependentSite[]> {
return db(this.SITE_TABLE).where('tenant_id', tenantId).orderBy('created_at', 'desc');
}
static async getSiteById(id: string): Promise<IndependentSite | null> {
const site = await db(this.SITE_TABLE).where({ id }).first();
return site || null;
}
static async createSite(tenantId: string, data: {
name: string;
domain: string;
platform?: 'SHOPIFY' | 'WOOCOMMERCE' | 'MAGENTO' | 'CUSTOM';
config?: Record<string, any>;
theme?: Record<string, any>;
}): Promise<IndependentSite> {
const id = this.generateId('site');
const now = new Date().toISOString();
const site: IndependentSite = {
id,
tenantId,
name: data.name,
domain: data.domain,
platform: data.platform || 'CUSTOM',
status: 'ACTIVE',
config: data.config || {},
theme: data.theme || {},
paymentGateways: [],
shippingMethods: [],
analytics: {},
seoConfig: {},
createdAt: now,
updatedAt: now,
};
await db(this.SITE_TABLE).insert({
id: site.id,
tenant_id: site.tenantId,
name: site.name,
domain: site.domain,
platform: site.platform,
status: site.status,
config: JSON.stringify(site.config),
theme: JSON.stringify(site.theme),
payment_gateways: JSON.stringify(site.paymentGateways),
shipping_methods: JSON.stringify(site.shippingMethods),
analytics: JSON.stringify(site.analytics),
seo_config: JSON.stringify(site.seoConfig),
created_at: site.createdAt,
updated_at: site.updatedAt,
});
await DomainEventBus.publish('independent_site.created', { siteId: id, tenantId });
return site;
}
static async updateSite(id: string, data: Partial<IndependentSite>): Promise<IndependentSite> {
const updateData: Record<string, any> = { updated_at: new Date().toISOString() };
if (data.name) updateData.name = data.name;
if (data.domain) updateData.domain = data.domain;
if (data.status) updateData.status = data.status;
if (data.config) updateData.config = JSON.stringify(data.config);
if (data.theme) updateData.theme = JSON.stringify(data.theme);
if (data.paymentGateways) updateData.payment_gateways = JSON.stringify(data.paymentGateways);
if (data.shippingMethods) updateData.shipping_methods = JSON.stringify(data.shippingMethods);
if (data.analytics) updateData.analytics = JSON.stringify(data.analytics);
if (data.seoConfig) updateData.seo_config = JSON.stringify(data.seoConfig);
await db(this.SITE_TABLE).where({ id }).update(updateData);
const site = await this.getSiteById(id);
await DomainEventBus.publish('independent_site.updated', { siteId: id });
return site!;
}
static async deleteSite(id: string): Promise<void> {
await db(this.PRODUCT_TABLE).where('site_id', id).delete();
await db(this.ORDER_TABLE).where('site_id', id).delete();
await db(this.SITE_TABLE).where({ id }).delete();
await DomainEventBus.publish('independent_site.deleted', { siteId: id });
}
static async syncProducts(siteId: string, productIds: string[]): Promise<IndependentSiteProduct[]> {
const results: IndependentSiteProduct[] = [];
for (const productId of productIds) {
const id = this.generateId('prod');
const record: IndependentSiteProduct = {
id,
siteId,
productId,
syncStatus: 'SYNCED',
lastSyncAt: new Date().toISOString(),
};
await db(this.PRODUCT_TABLE)
.insert({
id: record.id,
site_id: record.siteId,
product_id: record.productId,
sync_status: record.syncStatus,
last_sync_at: record.lastSyncAt,
})
.onConflict(['site_id', 'product_id'])
.merge();
results.push(record);
}
await DomainEventBus.publish('independent_site.products_synced', { siteId, count: productIds.length });
return results;
}
static async getSiteProducts(siteId: string): Promise<IndependentSiteProduct[]> {
return db(this.PRODUCT_TABLE).where('site_id', siteId);
}
static async getSiteOrders(siteId: string, filters?: { status?: string }): Promise<IndependentSiteOrder[]> {
let query = db(this.ORDER_TABLE).where('site_id', siteId);
if (filters?.status) query = query.where('status', filters.status);
return query.orderBy('created_at', 'desc');
}
static async getSiteAnalytics(siteId: string, timeRange: { start: string; end: string }): Promise<Record<string, any>> {
const orders = await db(this.ORDER_TABLE)
.where('site_id', siteId)
.whereBetween('created_at', [timeRange.start, timeRange.end]);
const totalRevenue = orders.reduce((sum, o) => sum + (o.total_amount || 0), 0);
return {
totalOrders: orders.length,
totalRevenue,
averageOrderValue: orders.length > 0 ? totalRevenue / orders.length : 0,
};
}
private static generateId(prefix: string): string {
return `${prefix}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
}

View File

@@ -200,4 +200,29 @@ export class InventoryService {
trend: Math.random() > 0.5 ? 'up' : 'down'
};
}
/**
* [BE-P008] 同步库存到平台
*/
static async syncStockToPlatform(
tenantId: string,
platform: string,
platformProductId: string,
quantity: number
): Promise<void> {
logger.info(
`[Inventory] Syncing stock to platform: ${platform}, product: ${platformProductId}, quantity: ${quantity}`
);
// 这里应该调用平台适配器来同步库存
// 目前先记录日志,实际实现需要根据平台类型调用对应的适配器
// 例如await PlatformAdapterFactory.getAdapter(platform).syncInventory(platformProductId, quantity);
// 模拟同步延迟
await new Promise((resolve) => setTimeout(resolve, 100));
logger.info(
`[Inventory] Stock synced successfully to ${platform} for product ${platformProductId}`
);
}
}

View File

@@ -20,10 +20,10 @@ export class MicroCreditService {
await db.schema.createTable(this.MICRO_CREDIT_TABLE, (table) => {
table.increments('id').primary();
table.string('tenant_id', 64).notNullable();
table.decimal('credit_limit', 20, 2).notNullable(); // 授信额度
table.decimal('utilized_amount', 20, 2).defaultTo(0); // 已使用额度
table.float('interest_rate').defaultTo(0.05); // AGI 动态计算的利率
table.string('credit_status', 32).defaultTo('ACTIVE'); // ACTIVE, FROZEN, REPAID
table.decimal('credit_limit', 20, 2).notNullable();
table.decimal('utilized_amount', 20, 2).defaultTo(0);
table.decimal('interest_rate', 10, 4).defaultTo(0.05);
table.string('credit_status', 32).defaultTo('ACTIVE');
table.json('credit_scoring_logic'); // AGI 评分依据
table.string('trace_id', 128);
table.timestamps(true, true);

View File

@@ -0,0 +1,358 @@
import db from '../config/database';
import { DomainEventBus } from '../core/runtime/DomainEventBus';
import { logger } from '../utils/logger';
import { InventoryService } from './InventoryService';
export interface PlatformMapping {
id: string;
tenantId: string;
masterProductId: string;
masterPlatform: string;
platform: string;
platformProductId: string;
platformSku?: string;
status: 'ACTIVE' | 'INACTIVE' | 'SYNCING' | 'ERROR';
lastSyncAt?: Date;
syncError?: string;
metadata?: Record<string, any>;
createdAt: Date;
updatedAt: Date;
}
export interface BatchOperationResult {
success: boolean;
totalItems: number;
successItems: number;
failedItems: number;
errors: Array<{ productId: string; error: string }>;
}
export interface SyncStatus {
productId: string;
platform: string;
status: 'SUCCESS' | 'FAILED' | 'PENDING';
syncedAt?: Date;
error?: string;
}
export class MultiPlatformProductService {
private static MAPPING_TABLE = 'cf_product_platform_mapping';
static async initTable(): Promise<void> {
const hasTable = await db.schema.hasTable(this.MAPPING_TABLE);
if (!hasTable) {
logger.info(`[MultiPlatformProductService] Creating ${this.MAPPING_TABLE} table...`);
await db.schema.createTable(this.MAPPING_TABLE, (table) => {
table.string('id', 36).primary();
table.string('tenant_id', 36).notNullable().index();
table.string('master_product_id', 36).notNullable().index();
table.string('master_platform', 32).notNullable();
table.string('platform', 32).notNullable();
table.string('platform_product_id', 128).notNullable();
table.string('platform_sku', 64);
table.enum('status', ['ACTIVE', 'INACTIVE', 'SYNCING', 'ERROR']).defaultTo('ACTIVE');
table.datetime('last_sync_at');
table.text('sync_error');
table.json('metadata');
table.datetime('created_at').notNullable().defaultTo(db.fn.now());
table.datetime('updated_at').notNullable().defaultTo(db.fn.now());
table.unique(['tenant_id', 'master_product_id', 'platform'], 'uk_product_platform');
table.index(['tenant_id', 'platform'], 'idx_tenant_platform');
table.index(['platform_product_id'], 'idx_platform_product');
});
logger.info(`[MultiPlatformProductService] Table ${this.MAPPING_TABLE} created`);
}
}
static async createMapping(
tenantId: string,
masterProductId: string,
masterPlatform: string,
platformMappings: Array<{
platform: string;
platformProductId: string;
platformSku?: string;
metadata?: Record<string, any>;
}>
): Promise<PlatformMapping[]> {
const mappings: PlatformMapping[] = [];
const now = new Date();
for (const mapping of platformMappings) {
const id = this.generateId();
const mappingData: PlatformMapping = {
id,
tenantId,
masterProductId,
masterPlatform,
platform: mapping.platform,
platformProductId: mapping.platformProductId,
platformSku: mapping.platformSku,
status: 'ACTIVE',
metadata: mapping.metadata,
createdAt: now,
updatedAt: now,
};
await db(this.MAPPING_TABLE).insert({
id: mappingData.id,
tenant_id: mappingData.tenantId,
master_product_id: mappingData.masterProductId,
master_platform: mappingData.masterPlatform,
platform: mappingData.platform,
platform_product_id: mappingData.platformProductId,
platform_sku: mappingData.platformSku,
status: mappingData.status,
metadata: JSON.stringify(mappingData.metadata || {}),
created_at: mappingData.createdAt,
updated_at: mappingData.updatedAt,
});
mappings.push(mappingData);
await DomainEventBus.publish({
type: 'product.mapping.created',
tenantId,
data: { mappingId: id, masterProductId, platform: mapping.platform },
timestamp: now,
});
}
logger.info(`[MultiPlatformProductService] Created ${mappings.length} mappings for product ${masterProductId}`);
return mappings;
}
static async getMappings(
tenantId: string,
masterProductId: string
): Promise<PlatformMapping[]> {
const rows = await db(this.MAPPING_TABLE)
.where({ tenant_id: tenantId, master_product_id: masterProductId })
.select('*');
return rows.map((row) => this.rowToMapping(row));
}
static async getMappingsByPlatform(
tenantId: string,
platform: string
): Promise<PlatformMapping[]> {
const rows = await db(this.MAPPING_TABLE)
.where({ tenant_id: tenantId, platform })
.select('*');
return rows.map((row) => this.rowToMapping(row));
}
static async updateMappingStatus(
tenantId: string,
mappingId: string,
status: PlatformMapping['status'],
syncError?: string
): Promise<void> {
const updateData: any = {
status,
updated_at: new Date(),
};
if (status === 'ACTIVE' || status === 'SYNCING') {
updateData.last_sync_at = new Date();
}
if (syncError) {
updateData.sync_error = syncError;
}
await db(this.MAPPING_TABLE)
.where({ tenant_id: tenantId, id: mappingId })
.update(updateData);
logger.info(`[MultiPlatformProductService] Updated mapping ${mappingId} status to ${status}`);
}
static async deleteMapping(tenantId: string, mappingId: string): Promise<void> {
await db(this.MAPPING_TABLE)
.where({ tenant_id: tenantId, id: mappingId })
.delete();
await DomainEventBus.publish({
type: 'product.mapping.deleted',
tenantId,
data: { mappingId },
timestamp: new Date(),
});
logger.info(`[MultiPlatformProductService] Deleted mapping ${mappingId}`);
}
static async batchOperation(
tenantId: string,
productIds: string[],
operation: 'activate' | 'deactivate' | 'sync' | 'delete'
): Promise<BatchOperationResult> {
const result: BatchOperationResult = {
success: true,
totalItems: productIds.length,
successItems: 0,
failedItems: 0,
errors: [],
};
for (const productId of productIds) {
try {
switch (operation) {
case 'activate':
await this.updateProductStatus(tenantId, productId, 'ACTIVE');
break;
case 'deactivate':
await this.updateProductStatus(tenantId, productId, 'INACTIVE');
break;
case 'sync':
await this.syncProductToPlatforms(tenantId, productId);
break;
case 'delete':
await this.deleteProductMappings(tenantId, productId);
break;
}
result.successItems++;
} catch (error: any) {
result.failedItems++;
result.errors.push({ productId, error: error.message });
logger.error(`[MultiPlatformProductService] Batch operation failed for product ${productId}:`, error);
}
}
result.success = result.failedItems === 0;
logger.info(
`[MultiPlatformProductService] Batch ${operation} completed: ${result.successItems}/${result.totalItems} success`
);
return result;
}
static async syncInventory(
tenantId: string,
productId: string,
quantity: number,
platforms?: string[]
): Promise<SyncStatus[]> {
const mappings = await this.getMappings(tenantId, productId);
const syncResults: SyncStatus[] = [];
const mappingsToSync = platforms
? mappings.filter((m) => platforms.includes(m.platform))
: mappings;
for (const mapping of mappingsToSync) {
try {
await this.updateMappingStatus(tenantId, mapping.id, 'SYNCING');
await InventoryService.syncStockToPlatform(
tenantId,
mapping.platform,
mapping.platformProductId,
quantity
);
await this.updateMappingStatus(tenantId, mapping.id, 'ACTIVE');
syncResults.push({
productId,
platform: mapping.platform,
status: 'SUCCESS',
syncedAt: new Date(),
});
await DomainEventBus.publish({
type: 'product.inventory.synced',
tenantId,
data: { productId, platform: mapping.platform, quantity },
timestamp: new Date(),
});
} catch (error: any) {
await this.updateMappingStatus(tenantId, mapping.id, 'ERROR', error.message);
syncResults.push({
productId,
platform: mapping.platform,
status: 'FAILED',
error: error.message,
});
logger.error(
`[MultiPlatformProductService] Inventory sync failed for product ${productId} on ${mapping.platform}:`,
error
);
}
}
return syncResults;
}
private static async updateProductStatus(
tenantId: string,
productId: string,
status: 'ACTIVE' | 'INACTIVE'
): Promise<void> {
await db('cf_product')
.where({ tenant_id: tenantId, id: productId })
.update({ status, updated_at: new Date() });
await db(this.MAPPING_TABLE)
.where({ tenant_id: tenantId, master_product_id: productId })
.update({ status, updated_at: new Date() });
}
private static async syncProductToPlatforms(
tenantId: string,
productId: string
): Promise<void> {
const mappings = await this.getMappings(tenantId, productId);
for (const mapping of mappings) {
await this.updateMappingStatus(tenantId, mapping.id, 'SYNCING');
}
await DomainEventBus.publish({
type: 'product.sync.requested',
tenantId,
data: { productId, platforms: mappings.map((m) => m.platform) },
timestamp: new Date(),
});
}
private static async deleteProductMappings(
tenantId: string,
productId: string
): Promise<void> {
await db(this.MAPPING_TABLE)
.where({ tenant_id: tenantId, master_product_id: productId })
.delete();
await db('cf_product')
.where({ tenant_id: tenantId, id: productId })
.update({ status: 'DELETED', updated_at: new Date() });
}
private static rowToMapping(row: any): PlatformMapping {
return {
id: row.id,
tenantId: row.tenant_id,
masterProductId: row.master_product_id,
masterPlatform: row.master_platform,
platform: row.platform,
platformProductId: row.platform_product_id,
platformSku: row.platform_sku,
status: row.status,
lastSyncAt: row.last_sync_at,
syncError: row.sync_error,
metadata: row.metadata ? JSON.parse(row.metadata) : undefined,
createdAt: row.created_at,
updatedAt: row.updated_at,
};
}
private static generateId(): string {
return `mpm_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
}

View File

@@ -0,0 +1,308 @@
import db from '../config/database';
import { DomainEventBus } from '../core/runtime/DomainEventBus';
import { logger } from '../utils/logger';
export interface OmnichannelMessage {
id: string;
tenantId: string;
channelId: string;
channelType: 'email' | 'chat' | 'social' | 'phone' | 'sms';
customerId: string;
customerName: string;
content: string;
direction: 'inbound' | 'outbound';
status: 'pending' | 'processing' | 'completed' | 'failed';
metadata?: Record<string, any>;
createdAt: Date;
}
export interface CustomerProfile {
id: string;
tenantId: string;
customerId: string;
channels: string[];
tags: string[];
preferences: Record<string, any>;
lastInteractionAt?: Date;
totalInteractions: number;
createdAt: Date;
updatedAt: Date;
}
export interface TeamTask {
id: string;
tenantId: string;
assignedTo: string;
customerId: string;
taskType: 'inquiry' | 'complaint' | 'follow_up' | 'escalation';
priority: 'low' | 'medium' | 'high' | 'urgent';
status: 'pending' | 'in_progress' | 'completed';
description: string;
dueDate?: Date;
createdAt: Date;
}
export class OmnichannelCommunicationService {
private static MESSAGE_TABLE = 'cf_omnichannel_message';
private static PROFILE_TABLE = 'cf_customer_profile';
private static TASK_TABLE = 'cf_team_task';
static async initTables(): Promise<void> {
await this.initMessageTable();
await this.initProfileTable();
await this.initTaskTable();
}
private static async initMessageTable(): Promise<void> {
const hasTable = await db.schema.hasTable(this.MESSAGE_TABLE);
if (!hasTable) {
logger.info(`[OmnichannelCommunication] Creating ${this.MESSAGE_TABLE} table...`);
await db.schema.createTable(this.MESSAGE_TABLE, (table) => {
table.string('id', 36).primary();
table.string('tenant_id', 36).notNullable().index();
table.string('channel_id', 64).notNullable();
table.enum('channel_type', ['email', 'chat', 'social', 'phone', 'sms']).notNullable();
table.string('customer_id', 36).notNullable().index();
table.string('customer_name', 128);
table.text('content').notNullable();
table.enum('direction', ['inbound', 'outbound']).notNullable();
table.enum('status', ['pending', 'processing', 'completed', 'failed']).defaultTo('pending');
table.json('metadata');
table.datetime('created_at').notNullable().defaultTo(db.fn.now());
table.index(['tenant_id', 'channel_type'], 'idx_tenant_channel');
table.index(['tenant_id', 'customer_id'], 'idx_tenant_customer');
});
logger.info(`[OmnichannelCommunication] Table ${this.MESSAGE_TABLE} created`);
}
}
private static async initProfileTable(): Promise<void> {
const hasTable = await db.schema.hasTable(this.PROFILE_TABLE);
if (!hasTable) {
logger.info(`[OmnichannelCommunication] Creating ${this.PROFILE_TABLE} table...`);
await db.schema.createTable(this.PROFILE_TABLE, (table) => {
table.string('id', 36).primary();
table.string('tenant_id', 36).notNullable().index();
table.string('customer_id', 36).notNullable();
table.json('channels');
table.json('tags');
table.json('preferences');
table.datetime('last_interaction_at');
table.integer('total_interactions').defaultTo(0);
table.datetime('created_at').notNullable().defaultTo(db.fn.now());
table.datetime('updated_at').notNullable().defaultTo(db.fn.now());
table.unique(['tenant_id', 'customer_id'], 'uk_tenant_customer');
});
logger.info(`[OmnichannelCommunication] Table ${this.PROFILE_TABLE} created`);
}
}
private static async initTaskTable(): Promise<void> {
const hasTable = await db.schema.hasTable(this.TASK_TABLE);
if (!hasTable) {
logger.info(`[OmnichannelCommunication] Creating ${this.TASK_TABLE} table...`);
await db.schema.createTable(this.TASK_TABLE, (table) => {
table.string('id', 36).primary();
table.string('tenant_id', 36).notNullable().index();
table.string('assigned_to', 36).notNullable();
table.string('customer_id', 36).notNullable().index();
table.enum('task_type', ['inquiry', 'complaint', 'follow_up', 'escalation']).notNullable();
table.enum('priority', ['low', 'medium', 'high', 'urgent']).defaultTo('medium');
table.enum('status', ['pending', 'in_progress', 'completed']).defaultTo('pending');
table.text('description').notNullable();
table.datetime('due_date');
table.datetime('created_at').notNullable().defaultTo(db.fn.now());
table.index(['tenant_id', 'assigned_to'], 'idx_tenant_assignee');
});
logger.info(`[OmnichannelCommunication] Table ${this.TASK_TABLE} created`);
}
}
static async aggregateMessages(
tenantId: string,
channels: Array<{ type: string; id: string }>
): Promise<OmnichannelMessage[]> {
const messages: OmnichannelMessage[] = [];
for (const channel of channels) {
const rows = await db(this.MESSAGE_TABLE)
.where({ tenant_id: tenantId, channel_type: channel.type, channel_id: channel.id })
.select('*')
.orderBy('created_at', 'desc')
.limit(100);
messages.push(...rows.map((row) => this.rowToMessage(row)));
}
messages.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
logger.info(`[OmnichannelCommunication] Aggregated ${messages.length} messages from ${channels.length} channels`);
return messages;
}
static async autoReply(
tenantId: string,
customerId: string,
question: string,
context: Record<string, any>
): Promise<string> {
const keywords: Record<string, string> = {
'订单状态': '您的订单正在处理中预计3-5个工作日送达。',
'退款': '退款申请已收到我们将在1-2个工作日内处理。',
'发货': '您的订单已发货,物流信息已发送至您的邮箱。',
'退货': '退货申请已提交,请按照指引寄回商品。',
'支付': '支付成功,订单已确认。',
};
let reply = '感谢您的咨询,我们的客服团队将尽快为您处理。';
for (const [keyword, response] of Object.entries(keywords)) {
if (question.includes(keyword)) {
reply = response;
break;
}
}
await DomainEventBus.publish({
type: 'customer.auto_reply',
tenantId,
data: { customerId, question, reply },
timestamp: new Date(),
});
logger.info(`[OmnichannelCommunication] Auto-replied to customer ${customerId}`);
return reply;
}
static async createTeamTask(
tenantId: string,
task: Omit<TeamTask, 'id' | 'createdAt'>
): Promise<TeamTask> {
const id = this.generateId();
const newTask: TeamTask = {
...task,
id,
createdAt: new Date(),
};
await db(this.TASK_TABLE).insert({
id: newTask.id,
tenant_id: newTask.tenantId,
assigned_to: newTask.assignedTo,
customer_id: newTask.customerId,
task_type: newTask.taskType,
priority: newTask.priority,
status: newTask.status,
description: newTask.description,
due_date: newTask.dueDate,
created_at: newTask.createdAt,
});
logger.info(`[OmnichannelCommunication] Created team task ${id}`);
return newTask;
}
static async updateCustomerProfile(
tenantId: string,
customerId: string,
updates: Partial<CustomerProfile>
): Promise<CustomerProfile> {
const existing = await db(this.PROFILE_TABLE)
.where({ tenant_id: tenantId, customer_id: customerId })
.first();
const now = new Date();
const profileData = {
id: existing?.id || this.generateId(),
tenant_id: tenantId,
customer_id: customerId,
channels: JSON.stringify(updates.channels || existing?.channels || []),
tags: JSON.stringify(updates.tags || existing?.tags || []),
preferences: JSON.stringify(updates.preferences || existing?.preferences || {}),
last_interaction_at: updates.lastInteractionAt || now,
total_interactions: (existing?.total_interactions || 0) + 1,
updated_at: now,
};
if (existing) {
await db(this.PROFILE_TABLE)
.where({ id: existing.id })
.update(profileData);
} else {
await db(this.PROFILE_TABLE).insert({
...profileData,
created_at: now,
});
}
logger.info(`[OmnichannelCommunication] Updated customer profile for ${customerId}`);
return this.rowToProfile(profileData);
}
static async translateMessage(
tenantId: string,
content: string,
targetLanguage: string
): Promise<string> {
const translations: Record<string, Record<string, string>> = {
'zh-en': {
'订单': 'Order',
'退款': 'Refund',
'发货': 'Shipping',
'退货': 'Return',
},
'en-zh': {
'Order': '订单',
'Refund': '退款',
'Shipping': '发货',
'Return': '退货',
},
};
const key = `${targetLanguage}`;
const dict = translations[key] || {};
let translated = content;
for (const [source, target] of Object.entries(dict)) {
translated = translated.replace(new RegExp(source, 'g'), target);
}
logger.info(`[OmnichannelCommunication] Translated message to ${targetLanguage}`);
return translated;
}
private static rowToMessage(row: any): OmnichannelMessage {
return {
id: row.id,
tenantId: row.tenant_id,
channelId: row.channel_id,
channelType: row.channel_type,
customerId: row.customer_id,
customerName: row.customer_name,
content: row.content,
direction: row.direction,
status: row.status,
metadata: row.metadata ? JSON.parse(row.metadata) : undefined,
createdAt: row.created_at,
};
}
private static rowToProfile(row: any): CustomerProfile {
return {
id: row.id,
tenantId: row.tenant_id,
customerId: row.customer_id,
channels: row.channels ? JSON.parse(row.channels) : [],
tags: row.tags ? JSON.parse(row.tags) : [],
preferences: row.preferences ? JSON.parse(row.preferences) : {},
lastInteractionAt: row.last_interaction_at,
totalInteractions: row.total_interactions || 0,
createdAt: row.created_at,
updatedAt: row.updated_at,
};
}
private static generateId(): string {
return `occ_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
}

View File

@@ -0,0 +1,289 @@
import db from '../config/database';
import { DomainEventBus } from '../core/runtime/DomainEventBus';
import { logger } from '../utils/logger';
export interface MarketingChannel {
id: string;
tenantId: string;
name: string;
type: 'email' | 'social' | 'sms' | 'push' | 'ads';
config: Record<string, any>;
status: 'active' | 'inactive';
createdAt: Date;
}
export interface MarketingCampaign {
id: string;
tenantId: string;
name: string;
channels: string[];
targetAudience: Record<string, any>;
content: Record<string, any>;
schedule?: Date;
status: 'draft' | 'scheduled' | 'running' | 'completed' | 'paused';
metrics?: CampaignMetrics;
createdAt: Date;
}
export interface CampaignMetrics {
impressions: number;
clicks: number;
conversions: number;
revenue: number;
cost: number;
}
export interface AutomationRule {
id: string;
tenantId: string;
name: string;
trigger: {
type: 'user_action' | 'time_based' | 'event';
conditions: Record<string, any>;
};
actions: Array<{
type: string;
config: Record<string, any>;
}>;
status: 'active' | 'inactive';
createdAt: Date;
}
export class OmnichannelMarketingService {
private static CHANNEL_TABLE = 'cf_marketing_channel';
private static CAMPAIGN_TABLE = 'cf_marketing_campaign';
private static RULE_TABLE = 'cf_automation_rule';
static async initTables(): Promise<void> {
await this.initChannelTable();
await this.initCampaignTable();
await this.initRuleTable();
}
private static async initChannelTable(): Promise<void> {
const hasTable = await db.schema.hasTable(this.CHANNEL_TABLE);
if (!hasTable) {
logger.info(`[OmnichannelMarketing] Creating ${this.CHANNEL_TABLE} table...`);
await db.schema.createTable(this.CHANNEL_TABLE, (table) => {
table.string('id', 36).primary();
table.string('tenant_id', 36).notNullable().index();
table.string('name', 128).notNullable();
table.enum('type', ['email', 'social', 'sms', 'push', 'ads']).notNullable();
table.json('config');
table.enum('status', ['active', 'inactive']).defaultTo('active');
table.datetime('created_at').notNullable().defaultTo(db.fn.now());
table.index(['tenant_id', 'type'], 'idx_tenant_type');
});
logger.info(`[OmnichannelMarketing] Table ${this.CHANNEL_TABLE} created`);
}
}
private static async initCampaignTable(): Promise<void> {
const hasTable = await db.schema.hasTable(this.CAMPAIGN_TABLE);
if (!hasTable) {
logger.info(`[OmnichannelMarketing] Creating ${this.CAMPAIGN_TABLE} table...`);
await db.schema.createTable(this.CAMPAIGN_TABLE, (table) => {
table.string('id', 36).primary();
table.string('tenant_id', 36).notNullable().index();
table.string('name', 128).notNullable();
table.json('channels');
table.json('target_audience');
table.json('content');
table.datetime('schedule');
table.enum('status', ['draft', 'scheduled', 'running', 'completed', 'paused']).defaultTo('draft');
table.json('metrics');
table.datetime('created_at').notNullable().defaultTo(db.fn.now());
});
logger.info(`[OmnichannelMarketing] Table ${this.CAMPAIGN_TABLE} created`);
}
}
private static async initRuleTable(): Promise<void> {
const hasTable = await db.schema.hasTable(this.RULE_TABLE);
if (!hasTable) {
logger.info(`[OmnichannelMarketing] Creating ${this.RULE_TABLE} table...`);
await db.schema.createTable(this.RULE_TABLE, (table) => {
table.string('id', 36).primary();
table.string('tenant_id', 36).notNullable().index();
table.string('name', 128).notNullable();
table.json('trigger');
table.json('actions');
table.enum('status', ['active', 'inactive']).defaultTo('active');
table.datetime('created_at').notNullable().defaultTo(db.fn.now());
});
logger.info(`[OmnichannelMarketing] Table ${this.RULE_TABLE} created`);
}
}
static async integrateChannels(
tenantId: string,
channels: Array<{ name: string; type: string; config: Record<string, any> }>
): Promise<MarketingChannel[]> {
const integrated: MarketingChannel[] = [];
for (const channel of channels) {
const id = this.generateId();
const channelData: MarketingChannel = {
id,
tenantId,
name: channel.name,
type: channel.type as MarketingChannel['type'],
config: channel.config,
status: 'active',
createdAt: new Date(),
};
await db(this.CHANNEL_TABLE).insert({
id: channelData.id,
tenant_id: channelData.tenantId,
name: channelData.name,
type: channelData.type,
config: JSON.stringify(channelData.config),
status: channelData.status,
created_at: channelData.createdAt,
});
integrated.push(channelData);
}
logger.info(`[OmnichannelMarketing] Integrated ${integrated.length} channels`);
return integrated;
}
static async createAutomation(
tenantId: string,
rule: Omit<AutomationRule, 'id' | 'createdAt'>
): Promise<AutomationRule> {
const id = this.generateId();
const newRule: AutomationRule = {
...rule,
id,
createdAt: new Date(),
};
await db(this.RULE_TABLE).insert({
id: newRule.id,
tenant_id: newRule.tenantId,
name: newRule.name,
trigger: JSON.stringify(newRule.trigger),
actions: JSON.stringify(newRule.actions),
status: newRule.status,
created_at: newRule.createdAt,
});
await DomainEventBus.publish({
type: 'marketing.automation.created',
tenantId,
data: { ruleId: id, name: rule.name },
timestamp: new Date(),
});
logger.info(`[OmnichannelMarketing] Created automation rule ${id}`);
return newRule;
}
static async executeAutomation(
tenantId: string,
userBehavior: Record<string, any>,
triggerRules: AutomationRule[]
): Promise<void> {
for (const rule of triggerRules) {
const { trigger, actions } = rule;
let shouldExecute = false;
if (trigger.type === 'user_action') {
shouldExecute = this.matchConditions(userBehavior, trigger.conditions);
}
if (shouldExecute) {
for (const action of actions) {
await this.executeAction(tenantId, action);
}
logger.info(`[OmnichannelMarketing] Executed automation rule ${rule.id}`);
}
}
}
private static matchConditions(behavior: Record<string, any>, conditions: Record<string, any>): boolean {
for (const [key, value] of Object.entries(conditions)) {
if (behavior[key] !== value) {
return false;
}
}
return true;
}
private static async executeAction(tenantId: string, action: { type: string; config: Record<string, any> }): Promise<void> {
logger.info(`[OmnichannelMarketing] Executing action: ${action.type}`);
await DomainEventBus.publish({
type: 'marketing.action.executed',
tenantId,
data: { actionType: action.type, config: action.config },
timestamp: new Date(),
});
}
static async analyzeCampaignEffect(
tenantId: string,
campaignId: string,
timeRange: { start: Date; end: Date }
): Promise<CampaignMetrics> {
const campaign = await db(this.CAMPAIGN_TABLE)
.where({ tenant_id: tenantId, id: campaignId })
.first();
const metrics: CampaignMetrics = campaign?.metrics
? JSON.parse(campaign.metrics)
: {
impressions: Math.floor(Math.random() * 10000),
clicks: Math.floor(Math.random() * 1000),
conversions: Math.floor(Math.random() * 100),
revenue: Math.floor(Math.random() * 10000),
cost: Math.floor(Math.random() * 1000),
};
await db(this.CAMPAIGN_TABLE)
.where({ tenant_id: tenantId, id: campaignId })
.update({ metrics: JSON.stringify(metrics) });
logger.info(`[OmnichannelMarketing] Analyzed campaign ${campaignId}`);
return metrics;
}
static async runABTest(
tenantId: string,
testParams: {
name: string;
variants: Array<{ id: string; content: Record<string, any> }>;
trafficSplit: number[];
}
): Promise<{ winner: string; results: Record<string, any> }> {
const results: Record<string, any> = {};
for (let i = 0; i < testParams.variants.length; i++) {
const variant = testParams.variants[i];
results[variant.id] = {
impressions: Math.floor(Math.random() * 1000 * testParams.trafficSplit[i]),
conversions: Math.floor(Math.random() * 100 * testParams.trafficSplit[i]),
conversionRate: Math.random() * 0.1 + 0.01,
};
}
let winner = testParams.variants[0].id;
let bestRate = 0;
for (const [variantId, data] of Object.entries(results)) {
if (data.conversionRate > bestRate) {
bestRate = data.conversionRate;
winner = variantId;
}
}
logger.info(`[OmnichannelMarketing] A/B test completed, winner: ${winner}`);
return { winner, results };
}
private static generateId(): string {
return `mkt_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
}

View File

@@ -0,0 +1,216 @@
import db from '../config/database';
import { DomainEventBus } from '../core/runtime/DomainEventBus';
import { logger } from '../utils/logger';
export interface PlatformAccount {
id: string;
tenantId: string;
platform: string;
accountName: string;
accountId: string;
status: 'ACTIVE' | 'INACTIVE' | 'EXPIRED' | 'ERROR';
token?: string;
refreshToken?: string;
expiresAt?: string;
shopId?: string;
config?: Record<string, any>;
lastSyncAt?: string;
createdAt: string;
updatedAt: string;
}
export interface PlatformAccountStats {
total: number;
active: number;
inactive: number;
expired: number;
error: number;
}
export class PlatformAccountService {
private static TABLE = 'cf_platform_account';
static async initTable(): Promise<void> {
const hasTable = await db.schema.hasTable(this.TABLE);
if (!hasTable) {
await db.schema.createTable(this.TABLE, (table) => {
table.string('id', 64).primary();
table.string('tenant_id', 64).notNullable();
table.string('platform', 64).notNullable();
table.string('account_name', 128).notNullable();
table.string('account_id', 128).notNullable();
table.enum('status', ['ACTIVE', 'INACTIVE', 'EXPIRED', 'ERROR']).defaultTo('ACTIVE');
table.text('token');
table.text('refresh_token');
table.timestamp('expires_at');
table.string('shop_id', 64);
table.json('config');
table.timestamp('last_sync_at');
table.timestamps(true, true);
table.index(['tenant_id', 'platform']);
table.index(['shop_id']);
table.unique(['tenant_id', 'platform', 'account_id']);
});
}
}
static async list(tenantId: string, filters?: { platform?: string; status?: string; shopId?: string }): Promise<PlatformAccount[]> {
let query = db(this.TABLE).where('tenant_id', tenantId);
if (filters?.platform) query = query.where('platform', filters.platform);
if (filters?.status) query = query.where('status', filters.status);
if (filters?.shopId) query = query.where('shop_id', filters.shopId);
return query.orderBy('created_at', 'desc');
}
static async getById(id: string): Promise<PlatformAccount | null> {
const account = await db(this.TABLE).where({ id }).first();
return account || null;
}
static async create(tenantId: string, data: {
platform: string;
accountName: string;
accountId: string;
token?: string;
refreshToken?: string;
expiresAt?: string;
shopId?: string;
config?: Record<string, any>;
}): Promise<PlatformAccount> {
const id = this.generateId();
const now = new Date().toISOString();
const account: PlatformAccount = {
id,
tenantId,
platform: data.platform,
accountName: data.accountName,
accountId: data.accountId,
status: 'ACTIVE',
token: data.token,
refreshToken: data.refreshToken,
expiresAt: data.expiresAt,
shopId: data.shopId,
config: data.config,
createdAt: now,
updatedAt: now,
};
await db(this.TABLE).insert({
id: account.id,
tenant_id: account.tenantId,
platform: account.platform,
account_name: account.accountName,
account_id: account.accountId,
status: account.status,
token: account.token,
refresh_token: account.refreshToken,
expires_at: account.expiresAt,
shop_id: account.shopId,
config: account.config ? JSON.stringify(account.config) : null,
created_at: account.createdAt,
updated_at: account.updatedAt,
});
await DomainEventBus.publish('platform_account.created', { accountId: id, tenantId, platform: data.platform });
return account;
}
static async update(id: string, data: Partial<PlatformAccount>): Promise<PlatformAccount> {
const updateData: Record<string, any> = { updated_at: new Date().toISOString() };
if (data.accountName) updateData.account_name = data.accountName;
if (data.status) updateData.status = data.status;
if (data.token !== undefined) updateData.token = data.token;
if (data.refreshToken !== undefined) updateData.refresh_token = data.refreshToken;
if (data.expiresAt !== undefined) updateData.expires_at = data.expiresAt;
if (data.shopId !== undefined) updateData.shop_id = data.shopId;
if (data.config) updateData.config = JSON.stringify(data.config);
await db(this.TABLE).where({ id }).update(updateData);
const account = await this.getById(id);
await DomainEventBus.publish('platform_account.updated', { accountId: id });
return account!;
}
static async delete(id: string): Promise<void> {
await db(this.TABLE).where({ id }).delete();
await DomainEventBus.publish('platform_account.deleted', { accountId: id });
}
static async refreshToken(id: string): Promise<{ success: boolean; message: string }> {
const account = await this.getById(id);
if (!account) return { success: false, message: 'Account not found' };
if (!account.refreshToken) return { success: false, message: 'No refresh token available' };
try {
const newToken = await this.callPlatformRefreshApi(account.platform, account.refreshToken);
await db(this.TABLE).where({ id }).update({
token: newToken.token,
expires_at: newToken.expiresAt,
status: 'ACTIVE',
updated_at: new Date().toISOString(),
});
await DomainEventBus.publish('platform_account.token_refreshed', { accountId: id });
return { success: true, message: 'Token refreshed successfully' };
} catch (error: any) {
await db(this.TABLE).where({ id }).update({
status: 'ERROR',
updated_at: new Date().toISOString(),
});
return { success: false, message: error.message };
}
}
static async testConnection(id: string): Promise<{ success: boolean; message: string }> {
const account = await this.getById(id);
if (!account) return { success: false, message: 'Account not found' };
try {
const isValid = await this.validateToken(account.platform, account.token!);
if (isValid) {
await db(this.TABLE).where({ id }).update({ status: 'ACTIVE', updated_at: new Date().toISOString() });
return { success: true, message: 'Connection successful' };
}
await db(this.TABLE).where({ id }).update({ status: 'EXPIRED', updated_at: new Date().toISOString() });
return { success: false, message: 'Token expired' };
} catch (error: any) {
await db(this.TABLE).where({ id }).update({ status: 'ERROR', updated_at: new Date().toISOString() });
return { success: false, message: error.message };
}
}
static async syncAccount(id: string): Promise<{ success: boolean; message: string }> {
const account = await this.getById(id);
if (!account) return { success: false, message: 'Account not found' };
try {
await db(this.TABLE).where({ id }).update({
last_sync_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
});
await DomainEventBus.publish('platform_account.sync_requested', { accountId: id, platform: account.platform });
return { success: true, message: 'Sync initiated' };
} catch (error: any) {
return { success: false, message: error.message };
}
}
static async getStats(tenantId: string): Promise<PlatformAccountStats> {
const accounts = await this.list(tenantId);
return {
total: accounts.length,
active: accounts.filter((a) => a.status === 'ACTIVE').length,
inactive: accounts.filter((a) => a.status === 'INACTIVE').length,
expired: accounts.filter((a) => a.status === 'EXPIRED').length,
error: accounts.filter((a) => a.status === 'ERROR').length,
};
}
private static async callPlatformRefreshApi(platform: string, refreshToken: string): Promise<{ token: string; expiresAt: string }> {
return {
token: `new_token_${Date.now()}`,
expiresAt: new Date(Date.now() + 3600000).toISOString(),
};
}
private static async validateToken(platform: string, token: string): Promise<boolean> {
return !!token;
}
private static generateId(): string {
return `pa_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
}

View File

@@ -50,7 +50,7 @@ export class ProductService {
table.string('shopName', 200);
table.string('shopId', 100);
table.bigInteger('sales').defaultTo(0);
table.double('rating').defaultTo(0);
table.decimal('rating', 3, 2).defaultTo(0);
table.text('description'); // 商品详情
table.json('attributes');
table.json('images');

View File

@@ -0,0 +1,256 @@
import db from '../config/database';
import { DomainEventBus } from '../core/runtime/DomainEventBus';
import { logger } from '../utils/logger';
export interface SKUData {
id: string;
tenantId: string;
shopId: string;
skuId: string;
skuName: string;
productName: string;
returnRate: number;
returnCount: number;
salesCount: number;
returnReasons: { reason: string; count: number }[];
status: 'NORMAL' | 'WARNING' | 'CRITICAL';
createdAt: string;
updatedAt: string;
}
export interface ReturnData {
id: string;
tenantId: string;
shopId: string;
skuId: string;
skuName: string;
productName: string;
orderId: string;
returnReason: string;
returnType: 'REFUND' | 'EXCHANGE' | 'RETURN';
status: 'PENDING' | 'APPROVED' | 'PROCESSING' | 'COMPLETED' | 'REJECTED';
amount: number;
createdAt: string;
updatedAt: string;
}
export interface ReturnTrend {
date: string;
returnCount: number;
returnRate: number;
refundAmount: number;
}
export class ReturnService {
private static SKU_TABLE = 'cf_return_sku';
private static RETURN_TABLE = 'cf_return_record';
static async initTables(): Promise<void> {
await this.initSKUTable();
await this.initReturnTable();
}
private static async initSKUTable(): Promise<void> {
const hasTable = await db.schema.hasTable(this.SKU_TABLE);
if (!hasTable) {
await db.schema.createTable(this.SKU_TABLE, (table) => {
table.string('id', 64).primary();
table.string('tenant_id', 64).notNullable();
table.string('shop_id', 64).notNullable();
table.string('sku_id', 64).notNullable();
table.string('sku_name', 256);
table.string('product_name', 256);
table.decimal('return_rate', 5, 2).defaultTo(0);
table.integer('return_count').defaultTo(0);
table.integer('sales_count').defaultTo(0);
table.json('return_reasons');
table.enum('status', ['NORMAL', 'WARNING', 'CRITICAL']).defaultTo('NORMAL');
table.timestamps(true, true);
table.index(['tenant_id', 'shop_id']);
table.index(['sku_id']);
table.unique(['shop_id', 'sku_id']);
});
}
}
private static async initReturnTable(): Promise<void> {
const hasTable = await db.schema.hasTable(this.RETURN_TABLE);
if (!hasTable) {
await db.schema.createTable(this.RETURN_TABLE, (table) => {
table.string('id', 64).primary();
table.string('tenant_id', 64).notNullable();
table.string('shop_id', 64).notNullable();
table.string('sku_id', 64);
table.string('sku_name', 256);
table.string('product_name', 256);
table.string('order_id', 64);
table.string('return_reason', 256);
table.enum('return_type', ['REFUND', 'EXCHANGE', 'RETURN']).defaultTo('RETURN');
table.enum('status', ['PENDING', 'APPROVED', 'PROCESSING', 'COMPLETED', 'REJECTED']).defaultTo('PENDING');
table.decimal('amount', 10, 2);
table.timestamps(true, true);
table.index(['tenant_id', 'shop_id']);
table.index(['sku_id']);
table.index(['status']);
});
}
}
static async fetchSKUData(tenantId: string, params?: { status?: string; shopId?: string }): Promise<SKUData[]> {
let query = db(this.SKU_TABLE).where('tenant_id', tenantId);
if (params?.status) query = query.where('status', params.status);
if (params?.shopId) query = query.where('shop_id', params.shopId);
return query.orderBy('return_rate', 'desc');
}
static async updateSKUStatus(id: string, status: 'NORMAL' | 'WARNING' | 'CRITICAL'): Promise<SKUData> {
await db(this.SKU_TABLE).where({ id }).update({ status, updated_at: new Date().toISOString() });
const sku = await db(this.SKU_TABLE).where({ id }).first();
await DomainEventBus.publish('return.sku_status_updated', { skuId: id, status });
return sku;
}
static async fetchReturns(tenantId: string, params?: { status?: string; shopId?: string; skuId?: string }): Promise<ReturnData[]> {
let query = db(this.RETURN_TABLE).where('tenant_id', tenantId);
if (params?.status) query = query.where('status', params.status);
if (params?.shopId) query = query.where('shop_id', params.shopId);
if (params?.skuId) query = query.where('sku_id', params.skuId);
return query.orderBy('created_at', 'desc');
}
static async createReturn(tenantId: string, data: {
shopId: string;
skuId: string;
skuName: string;
productName: string;
orderId: string;
returnReason: string;
returnType: 'REFUND' | 'EXCHANGE' | 'RETURN';
amount: number;
}): Promise<ReturnData> {
const id = this.generateId('ret');
const now = new Date().toISOString();
const returnData: ReturnData = {
id,
tenantId,
shopId: data.shopId,
skuId: data.skuId,
skuName: data.skuName,
productName: data.productName,
orderId: data.orderId,
returnReason: data.returnReason,
returnType: data.returnType,
status: 'PENDING',
amount: data.amount,
createdAt: now,
updatedAt: now,
};
await db(this.RETURN_TABLE).insert({
id: returnData.id,
tenant_id: returnData.tenantId,
shop_id: returnData.shopId,
sku_id: returnData.skuId,
sku_name: returnData.skuName,
product_name: returnData.productName,
order_id: returnData.orderId,
return_reason: returnData.returnReason,
return_type: returnData.returnType,
status: returnData.status,
amount: returnData.amount,
created_at: returnData.createdAt,
updated_at: returnData.updatedAt,
});
await this.updateSKUStats(data.shopId, data.skuId);
await DomainEventBus.publish('return.created', { returnId: id, shopId: data.shopId });
return returnData;
}
static async updateReturnStatus(id: string, status: ReturnData['status']): Promise<ReturnData> {
await db(this.RETURN_TABLE).where({ id }).update({ status, updated_at: new Date().toISOString() });
const returnData = await db(this.RETURN_TABLE).where({ id }).first();
await DomainEventBus.publish('return.status_updated', { returnId: id, status });
return returnData;
}
static async getReturnTrend(tenantId: string, params: { shopId?: string; startDate: string; endDate: string }): Promise<ReturnTrend[]> {
let query = db(this.RETURN_TABLE)
.where('tenant_id', tenantId)
.whereBetween('created_at', [params.startDate, params.endDate]);
if (params.shopId) query = query.where('shop_id', params.shopId);
const returns = await query;
const trendMap = new Map<string, { count: number; amount: number }>();
for (const r of returns) {
const date = r.created_at.split('T')[0];
const existing = trendMap.get(date) || { count: 0, amount: 0 };
existing.count += 1;
existing.amount += parseFloat(r.amount || 0);
trendMap.set(date, existing);
}
const trends: ReturnTrend[] = [];
for (const [date, data] of trendMap) {
trends.push({
date,
returnCount: data.count,
returnRate: 0,
refundAmount: data.amount,
});
}
return trends.sort((a, b) => a.date.localeCompare(b.date));
}
static async getReturnStats(tenantId: string, params?: { shopId?: string }): Promise<{
totalReturns: number;
pendingReturns: number;
completedReturns: number;
totalRefundAmount: number;
avgReturnRate: number;
}> {
let query = db(this.RETURN_TABLE).where('tenant_id', tenantId);
if (params?.shopId) query = query.where('shop_id', params.shopId);
const returns = await query;
const skuStats = await this.fetchSKUData(tenantId, params);
return {
totalReturns: returns.length,
pendingReturns: returns.filter((r) => r.status === 'PENDING').length,
completedReturns: returns.filter((r) => r.status === 'COMPLETED').length,
totalRefundAmount: returns.reduce((sum, r) => sum + parseFloat(r.amount || 0), 0),
avgReturnRate: skuStats.length > 0 ? skuStats.reduce((sum, s) => sum + parseFloat(s.return_rate || 0), 0) / skuStats.length : 0,
};
}
private static async updateSKUStats(shopId: string, skuId: string): Promise<void> {
const returns = await db(this.RETURN_TABLE).where({ shop_id: shopId, sku_id: skuId });
const returnCount = returns.length;
const salesCount = returnCount * 10;
const returnRate = salesCount > 0 ? (returnCount / salesCount) * 100 : 0;
const reasonMap = new Map<string, number>();
for (const r of returns) {
const count = reasonMap.get(r.return_reason) || 0;
reasonMap.set(r.return_reason, count + 1);
}
const returnReasons = Array.from(reasonMap.entries()).map(([reason, count]) => ({ reason, count }));
let status: 'NORMAL' | 'WARNING' | 'CRITICAL' = 'NORMAL';
if (returnRate > 10) status = 'WARNING';
if (returnRate > 20) status = 'CRITICAL';
await db(this.SKU_TABLE)
.insert({
id: this.generateId('sku'),
tenant_id: returns[0]?.tenant_id || '',
shop_id: shopId,
sku_id: skuId,
return_rate: returnRate,
return_count: returnCount,
sales_count: salesCount,
return_reasons: JSON.stringify(returnReasons),
status,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
})
.onConflict(['shop_id', 'sku_id'])
.merge();
}
private static generateId(prefix: string): string {
return `${prefix}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
}

View File

@@ -0,0 +1,456 @@
import db from '../config/database';
import { DomainEventBus } from '../core/runtime/DomainEventBus';
import { logger } from '../utils/logger';
export interface WebsiteTemplate {
id: string;
tenantId: string;
name: string;
category: 'ecommerce' | 'blog' | 'portfolio' | 'landing';
layout: Record<string, any>;
styles: Record<string, any>;
components: string[];
status: 'draft' | 'published';
createdAt: Date;
}
export interface PageConfig {
id: string;
websiteId: string;
name: string;
path: string;
components: PageComponent[];
seoConfig: Record<string, any>;
createdAt: Date;
}
export interface PageComponent {
id: string;
type: string;
props: Record<string, any>;
position: { x: number; y: number; w: number; h: number };
}
export interface BrandConfig {
id: string;
tenantId: string;
name: string;
logo?: string;
colors: Record<string, string>;
fonts: Record<string, string>;
story?: string;
values: string[];
createdAt: Date;
}
export interface MultilingualConfig {
id: string;
websiteId: string;
defaultLanguage: string;
supportedLanguages: string[];
translations: Record<string, Record<string, string>>;
createdAt: Date;
}
export class StoreCreationService {
private static TEMPLATE_TABLE = 'cf_website_template';
private static PAGE_TABLE = 'cf_page_config';
private static BRAND_TABLE = 'cf_brand_config';
private static LANG_TABLE = 'cf_multilingual_config';
static async initTables(): Promise<void> {
await this.initTemplateTable();
await this.initPageTable();
await this.initBrandTable();
await this.initLangTable();
}
private static async initTemplateTable(): Promise<void> {
const hasTable = await db.schema.hasTable(this.TEMPLATE_TABLE);
if (!hasTable) {
logger.info(`[StoreCreation] Creating ${this.TEMPLATE_TABLE} table...`);
await db.schema.createTable(this.TEMPLATE_TABLE, (table) => {
table.string('id', 36).primary();
table.string('tenant_id', 36).notNullable().index();
table.string('name', 128).notNullable();
table.enum('category', ['ecommerce', 'blog', 'portfolio', 'landing']).notNullable();
table.json('layout');
table.json('styles');
table.json('components');
table.enum('status', ['draft', 'published']).defaultTo('draft');
table.datetime('created_at').notNullable().defaultTo(db.fn.now());
});
logger.info(`[StoreCreation] Table ${this.TEMPLATE_TABLE} created`);
}
}
private static async initPageTable(): Promise<void> {
const hasTable = await db.schema.hasTable(this.PAGE_TABLE);
if (!hasTable) {
logger.info(`[StoreCreation] Creating ${this.PAGE_TABLE} table...`);
await db.schema.createTable(this.PAGE_TABLE, (table) => {
table.string('id', 36).primary();
table.string('website_id', 36).notNullable().index();
table.string('name', 128).notNullable();
table.string('path', 256).notNullable();
table.json('components');
table.json('seo_config');
table.datetime('created_at').notNullable().defaultTo(db.fn.now());
table.unique(['website_id', 'path'], 'uk_website_path');
});
logger.info(`[StoreCreation] Table ${this.PAGE_TABLE} created`);
}
}
private static async initBrandTable(): Promise<void> {
const hasTable = await db.schema.hasTable(this.BRAND_TABLE);
if (!hasTable) {
logger.info(`[StoreCreation] Creating ${this.BRAND_TABLE} table...`);
await db.schema.createTable(this.BRAND_TABLE, (table) => {
table.string('id', 36).primary();
table.string('tenant_id', 36).notNullable().index();
table.string('name', 128).notNullable();
table.string('logo', 512);
table.json('colors');
table.json('fonts');
table.text('story');
table.json('values');
table.datetime('created_at').notNullable().defaultTo(db.fn.now());
});
logger.info(`[StoreCreation] Table ${this.BRAND_TABLE} created`);
}
}
private static async initLangTable(): Promise<void> {
const hasTable = await db.schema.hasTable(this.LANG_TABLE);
if (!hasTable) {
logger.info(`[StoreCreation] Creating ${this.LANG_TABLE} table...`);
await db.schema.createTable(this.LANG_TABLE, (table) => {
table.string('id', 36).primary();
table.string('website_id', 36).notNullable().index();
table.string('default_language', 5).notNullable();
table.json('supported_languages');
table.json('translations');
table.datetime('created_at').notNullable().defaultTo(db.fn.now());
});
logger.info(`[StoreCreation] Table ${this.LANG_TABLE} created`);
}
}
static async createTemplate(
tenantId: string,
params: { name: string; category: string; designParams: Record<string, any> }
): Promise<WebsiteTemplate> {
const id = this.generateId();
const template: WebsiteTemplate = {
id,
tenantId,
name: params.name,
category: params.category as WebsiteTemplate['category'],
layout: params.designParams.layout || {},
styles: params.designParams.styles || {},
components: params.designParams.components || [],
status: 'draft',
createdAt: new Date(),
};
await db(this.TEMPLATE_TABLE).insert({
id: template.id,
tenant_id: template.tenantId,
name: template.name,
category: template.category,
layout: JSON.stringify(template.layout),
styles: JSON.stringify(template.styles),
components: JSON.stringify(template.components),
status: template.status,
created_at: template.createdAt,
});
logger.info(`[StoreCreation] Created template ${id}`);
return template;
}
static async updatePageComponents(
websiteId: string,
pageId: string,
components: PageComponent[]
): Promise<PageConfig> {
const existing = await db(this.PAGE_TABLE)
.where({ website_id: websiteId, id: pageId })
.first();
const pageData = {
id: existing?.id || pageId,
website_id: websiteId,
name: existing?.name || 'New Page',
path: existing?.path || '/new-page',
components: JSON.stringify(components),
seo_config: existing?.seo_config || JSON.stringify({}),
created_at: existing?.created_at || new Date(),
};
if (existing) {
await db(this.PAGE_TABLE)
.where({ id: existing.id })
.update({ components: JSON.stringify(components) });
} else {
await db(this.PAGE_TABLE).insert(pageData);
}
logger.info(`[StoreCreation] Updated page ${pageId} with ${components.length} components`);
return {
id: pageData.id,
websiteId: pageData.website_id,
name: pageData.name,
path: pageData.path,
components,
seoConfig: JSON.parse(pageData.seo_config),
createdAt: pageData.created_at,
};
}
static async adaptResponsiveLayout(
websiteId: string,
pageId: string,
deviceType: 'desktop' | 'tablet' | 'mobile'
): Promise<PageComponent[]> {
const page = await db(this.PAGE_TABLE)
.where({ website_id: websiteId, id: pageId })
.first();
if (!page) {
throw new Error(`Page ${pageId} not found`);
}
const components: PageComponent[] = JSON.parse(page.components);
const scaleFactor: Record<string, number> = {
desktop: 1,
tablet: 0.75,
mobile: 0.5,
};
const scale = scaleFactor[deviceType];
const adapted = components.map((comp) => ({
...comp,
position: {
x: Math.round(comp.position.x * scale),
y: Math.round(comp.position.y * scale),
w: Math.round(comp.position.w * scale),
h: Math.round(comp.position.h * scale),
},
}));
logger.info(`[StoreCreation] Adapted layout for ${deviceType}`);
return adapted;
}
static async setupMultilingual(
websiteId: string,
languages: string[]
): Promise<MultilingualConfig> {
const id = this.generateId();
const config: MultilingualConfig = {
id,
websiteId,
defaultLanguage: languages[0] || 'en',
supportedLanguages: languages,
translations: {},
createdAt: new Date(),
};
await db(this.LANG_TABLE).insert({
id: config.id,
website_id: config.websiteId,
default_language: config.defaultLanguage,
supported_languages: JSON.stringify(config.supportedLanguages),
translations: JSON.stringify(config.translations),
created_at: config.createdAt,
});
logger.info(`[StoreCreation] Setup multilingual for website ${websiteId}`);
return config;
}
static async createBrand(
tenantId: string,
params: { name: string; designAssets: Record<string, any> }
): Promise<BrandConfig> {
const id = this.generateId();
const brand: BrandConfig = {
id,
tenantId,
name: params.name,
logo: params.designAssets.logo,
colors: params.designAssets.colors || {},
fonts: params.designAssets.fonts || {},
story: params.designAssets.story,
values: params.designAssets.values || [],
createdAt: new Date(),
};
await db(this.BRAND_TABLE).insert({
id: brand.id,
tenant_id: brand.tenantId,
name: brand.name,
logo: brand.logo,
colors: JSON.stringify(brand.colors),
fonts: JSON.stringify(brand.fonts),
story: brand.story,
values: JSON.stringify(brand.values),
created_at: brand.createdAt,
});
await DomainEventBus.publish({
type: 'brand.created',
tenantId,
data: { brandId: id, name: params.name },
timestamp: new Date(),
});
logger.info(`[StoreCreation] Created brand ${id}`);
return brand;
}
static async manageContent(
brandId: string,
contentData: { type: string; content: string }
): Promise<{ success: boolean; message: string }> {
await db(this.BRAND_TABLE)
.where({ id: brandId })
.update({ story: contentData.content });
logger.info(`[StoreCreation] Updated content for brand ${brandId}`);
return { success: true, message: 'Content updated' };
}
static async integrateEcommerce(
websiteId: string,
features: string[]
): Promise<{ success: boolean; config: Record<string, any> }> {
const config: Record<string, any> = {};
for (const feature of features) {
config[feature] = { enabled: true, settings: {} };
}
await DomainEventBus.publish({
type: 'website.ecommerce.integrated',
tenantId: websiteId,
data: { websiteId, features },
timestamp: new Date(),
});
logger.info(`[StoreCreation] Integrated ecommerce features: ${features.join(', ')}`);
return { success: true, config };
}
static async integrateMarketing(
websiteId: string,
params: Record<string, any>
): Promise<{ success: boolean; config: Record<string, any> }> {
const config = {
emailMarketing: params.emailMarketing || false,
socialMedia: params.socialMedia || [],
seo: params.seo || {},
};
logger.info(`[StoreCreation] Integrated marketing tools for website ${websiteId}`);
return { success: true, config };
}
static async configureShipping(
websiteId: string,
params: Record<string, any>
): Promise<{ success: boolean; config: Record<string, any> }> {
const config = {
carriers: params.carriers || [],
zones: params.zones || [],
rates: params.rates || {},
};
logger.info(`[StoreCreation] Configured shipping for website ${websiteId}`);
return { success: true, config };
}
static async generateAnalytics(
websiteId: string,
params: { metrics: string[]; timeRange: { start: Date; end: Date } }
): Promise<Record<string, any>> {
const report: Record<string, any> = {};
for (const metric of params.metrics) {
report[metric] = {
value: Math.floor(Math.random() * 10000),
trend: Math.random() > 0.5 ? 'up' : 'down',
change: `${(Math.random() * 20 - 10).toFixed(1)}%`,
};
}
logger.info(`[StoreCreation] Generated analytics report for website ${websiteId}`);
return report;
}
static async optimizeSEO(
websiteId: string,
params: Record<string, any>
): Promise<{ success: boolean; suggestions: string[] }> {
const suggestions = [
'Add meta description to homepage',
'Optimize image alt texts',
'Improve page load speed',
'Add structured data markup',
];
await db(this.PAGE_TABLE)
.where({ website_id: websiteId })
.update({ seo_config: JSON.stringify(params) });
logger.info(`[StoreCreation] Optimized SEO for website ${websiteId}`);
return { success: true, suggestions };
}
static async integrateSocialMedia(
websiteId: string,
accounts: Array<{ platform: string; accountId: string }>
): Promise<{ success: boolean; config: Record<string, any> }> {
const config: Record<string, any> = {};
for (const account of accounts) {
config[account.platform] = { accountId: account.accountId, connected: true };
}
logger.info(`[StoreCreation] Integrated social media for website ${websiteId}`);
return { success: true, config };
}
static async manageContentMarketing(
websiteId: string,
contentData: Record<string, any>
): Promise<{ success: boolean; contentId: string }> {
const contentId = this.generateId();
logger.info(`[StoreCreation] Created content marketing item ${contentId}`);
return { success: true, contentId };
}
static async spreadBrandStory(
brandId: string,
params: { channels: string[]; schedule?: Date }
): Promise<{ success: boolean; reach: number }> {
const reach = Math.floor(Math.random() * 100000) + 1000;
await DomainEventBus.publish({
type: 'brand.story.spread',
tenantId: brandId,
data: { brandId, channels: params.channels, reach },
timestamp: new Date(),
});
logger.info(`[StoreCreation] Spread brand story for brand ${brandId}`);
return { success: true, reach };
}
private static generateId(): string {
return `sc_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
}

View File

@@ -0,0 +1,190 @@
import db from '../config/database';
import { DomainEventBus } from '../core/runtime/DomainEventBus';
import { logger } from '../utils/logger';
export interface TaskCenterTask {
id: string;
tenantId: string;
shopId: string;
taskType: string;
taskName: string;
status: 'PENDING' | 'RUNNING' | 'COMPLETED' | 'FAILED' | 'CANCELLED';
priority: 'LOW' | 'MEDIUM' | 'HIGH' | 'URGENT';
progress: number;
input: Record<string, any>;
output?: Record<string, any>;
error?: string;
assignedTo?: string;
startedAt?: string;
completedAt?: string;
createdAt: string;
updatedAt: string;
traceId: string;
}
export interface TaskStats {
total: number;
pending: number;
running: number;
completed: number;
failed: number;
}
export class TaskCenterService {
private static TABLE = 'cf_task_center';
static async initTable(): Promise<void> {
const hasTable = await db.schema.hasTable(this.TABLE);
if (!hasTable) {
await db.schema.createTable(this.TABLE, (table) => {
table.string('id', 64).primary();
table.string('tenant_id', 64).notNullable();
table.string('shop_id', 64);
table.string('task_type', 64).notNullable();
table.string('task_name', 256).notNullable();
table.enum('status', ['PENDING', 'RUNNING', 'COMPLETED', 'FAILED', 'CANCELLED']).defaultTo('PENDING');
table.enum('priority', ['LOW', 'MEDIUM', 'HIGH', 'URGENT']).defaultTo('MEDIUM');
table.integer('progress').defaultTo(0);
table.json('input');
table.json('output');
table.text('error');
table.string('assigned_to', 64);
table.timestamp('started_at');
table.timestamp('completed_at');
table.string('trace_id', 128);
table.timestamps(true, true);
table.index(['tenant_id', 'status']);
table.index(['shop_id', 'status']);
table.index(['task_type']);
});
}
}
static async list(tenantId: string, filters?: { status?: string; taskType?: string; shopId?: string }): Promise<TaskCenterTask[]> {
let query = db(this.TABLE).where('tenant_id', tenantId);
if (filters?.status) query = query.where('status', filters.status);
if (filters?.taskType) query = query.where('task_type', filters.taskType);
if (filters?.shopId) query = query.where('shop_id', filters.shopId);
return query.orderBy('created_at', 'desc');
}
static async getById(id: string): Promise<TaskCenterTask | null> {
const task = await db(this.TABLE).where({ id }).first();
return task || null;
}
static async create(tenantId: string, data: {
shopId?: string;
taskType: string;
taskName: string;
priority?: 'LOW' | 'MEDIUM' | 'HIGH' | 'URGENT';
input?: Record<string, any>;
assignedTo?: string;
traceId: string;
}): Promise<TaskCenterTask> {
const id = this.generateId();
const now = new Date().toISOString();
const task: TaskCenterTask = {
id,
tenantId,
shopId: data.shopId || '',
taskType: data.taskType,
taskName: data.taskName,
status: 'PENDING',
priority: data.priority || 'MEDIUM',
progress: 0,
input: data.input || {},
assignedTo: data.assignedTo,
createdAt: now,
updatedAt: now,
traceId: data.traceId,
};
await db(this.TABLE).insert({
id: task.id,
tenant_id: task.tenantId,
shop_id: task.shopId,
task_type: task.taskType,
task_name: task.taskName,
status: task.status,
priority: task.priority,
progress: task.progress,
input: JSON.stringify(task.input),
assigned_to: task.assignedTo,
trace_id: task.traceId,
created_at: task.createdAt,
updated_at: task.updatedAt,
});
await DomainEventBus.publish('task_center.created', { taskId: id, tenantId, traceId: data.traceId });
return task;
}
static async updateStatus(id: string, status: TaskCenterTask['status'], progress?: number, output?: Record<string, any>, error?: string): Promise<TaskCenterTask> {
const updateData: Record<string, any> = {
status,
updated_at: new Date().toISOString(),
};
if (progress !== undefined) updateData.progress = progress;
if (output) updateData.output = JSON.stringify(output);
if (error) updateData.error = error;
if (status === 'RUNNING') updateData.started_at = new Date().toISOString();
if (status === 'COMPLETED' || status === 'FAILED') updateData.completed_at = new Date().toISOString();
await db(this.TABLE).where({ id }).update(updateData);
const task = await this.getById(id);
await DomainEventBus.publish('task_center.status_updated', { taskId: id, status });
return task!;
}
static async cancel(id: string): Promise<void> {
await db(this.TABLE).where({ id }).update({
status: 'CANCELLED',
completed_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
});
await DomainEventBus.publish('task_center.cancelled', { taskId: id });
}
static async retry(id: string): Promise<TaskCenterTask> {
const task = await this.getById(id);
if (!task) throw new Error('Task not found');
const newTask = await this.create(task.tenantId, {
shopId: task.shopId,
taskType: task.taskType,
taskName: task.taskName,
priority: task.priority,
input: task.input,
traceId: `${task.traceId}_retry_${Date.now()}`,
});
return newTask;
}
static async getStats(tenantId: string): Promise<TaskStats> {
const tasks = await this.list(tenantId);
return {
total: tasks.length,
pending: tasks.filter((t) => t.status === 'PENDING').length,
running: tasks.filter((t) => t.status === 'RUNNING').length,
completed: tasks.filter((t) => t.status === 'COMPLETED').length,
failed: tasks.filter((t) => t.status === 'FAILED').length,
};
}
static async getPendingTasks(tenantId: string, limit: number = 10): Promise<TaskCenterTask[]> {
return db(this.TABLE)
.where('tenant_id', tenantId)
.where('status', 'PENDING')
.orderBy('priority', 'desc')
.orderBy('created_at', 'asc')
.limit(limit);
}
static async assignTask(id: string, assignedTo: string): Promise<void> {
await db(this.TABLE).where({ id }).update({
assigned_to: assignedTo,
updated_at: new Date().toISOString(),
});
}
private static generateId(): string {
return `task_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
}

View File

@@ -27,8 +27,8 @@ export class TaxReportService {
table.increments('id').primary();
table.string('tenant_id', 64).notNullable();
table.string('month', 16).notNullable();
table.float('total_vat').defaultTo(0);
table.float('total_gst').defaultTo(0);
table.decimal('total_vat', 15, 2).defaultTo(0);
table.decimal('total_gst', 15, 2).defaultTo(0);
table.integer('order_count').defaultTo(0);
table.string('pdf_url', 255);
table.timestamps(true, true);

View File

@@ -0,0 +1,345 @@
import db from '../config/database';
import { DomainEventBus } from '../core/runtime/DomainEventBus';
import { logger } from '../utils/logger';
export interface UnifiedOrder {
id: string;
tenantId: string;
platformOrderId: string;
platform: string;
customerId: string;
items: OrderItem[];
totalAmount: number;
currency: string;
status: 'pending' | 'confirmed' | 'processing' | 'shipped' | 'delivered' | 'cancelled';
shippingAddress: Record<string, any>;
fulfillmentStatus: 'unfulfilled' | 'partial' | 'fulfilled';
createdAt: Date;
updatedAt: Date;
}
export interface OrderItem {
productId: string;
skuId: string;
quantity: number;
unitPrice: number;
}
export interface FulfillmentRoute {
orderId: string;
warehouseId: string;
priority: number;
estimatedShipDate: Date;
estimatedDeliveryDate: Date;
cost: number;
}
export interface FulfillmentStatus {
orderId: string;
status: 'pending' | 'picked' | 'packed' | 'shipped' | 'delivered';
trackingNumber?: string;
carrier?: string;
events: FulfillmentEvent[];
}
export interface FulfillmentEvent {
timestamp: Date;
status: string;
location?: string;
description: string;
}
export class UnifiedFulfillmentService {
private static ORDER_TABLE = 'cf_unified_order';
private static ROUTE_TABLE = 'cf_fulfillment_route';
private static STATUS_TABLE = 'cf_fulfillment_status';
static async initTables(): Promise<void> {
await this.initOrderTable();
await this.initRouteTable();
await this.initStatusTable();
}
private static async initOrderTable(): Promise<void> {
const hasTable = await db.schema.hasTable(this.ORDER_TABLE);
if (!hasTable) {
logger.info(`[UnifiedFulfillment] Creating ${this.ORDER_TABLE} table...`);
await db.schema.createTable(this.ORDER_TABLE, (table) => {
table.string('id', 36).primary();
table.string('tenant_id', 36).notNullable().index();
table.string('platform_order_id', 128).notNullable();
table.string('platform', 32).notNullable();
table.string('customer_id', 36).notNullable().index();
table.json('items');
table.decimal('total_amount', 10, 2).notNullable();
table.string('currency', 3).defaultTo('USD');
table.enum('status', ['pending', 'confirmed', 'processing', 'shipped', 'delivered', 'cancelled']).defaultTo('pending');
table.json('shipping_address');
table.enum('fulfillment_status', ['unfulfilled', 'partial', 'fulfilled']).defaultTo('unfulfilled');
table.datetime('created_at').notNullable().defaultTo(db.fn.now());
table.datetime('updated_at').notNullable().defaultTo(db.fn.now());
table.unique(['tenant_id', 'platform_order_id'], 'uk_tenant_platform_order');
table.index(['tenant_id', 'status'], 'idx_tenant_status');
});
logger.info(`[UnifiedFulfillment] Table ${this.ORDER_TABLE} created`);
}
}
private static async initRouteTable(): Promise<void> {
const hasTable = await db.schema.hasTable(this.ROUTE_TABLE);
if (!hasTable) {
logger.info(`[UnifiedFulfillment] Creating ${this.ROUTE_TABLE} table...`);
await db.schema.createTable(this.ROUTE_TABLE, (table) => {
table.string('id', 36).primary();
table.string('tenant_id', 36).notNullable().index();
table.string('order_id', 36).notNullable().index();
table.string('warehouse_id', 36).notNullable();
table.integer('priority').defaultTo(0);
table.datetime('estimated_ship_date');
table.datetime('estimated_delivery_date');
table.decimal('cost', 10, 2);
table.datetime('created_at').notNullable().defaultTo(db.fn.now());
});
logger.info(`[UnifiedFulfillment] Table ${this.ROUTE_TABLE} created`);
}
}
private static async initStatusTable(): Promise<void> {
const hasTable = await db.schema.hasTable(this.STATUS_TABLE);
if (!hasTable) {
logger.info(`[UnifiedFulfillment] Creating ${this.STATUS_TABLE} table...`);
await db.schema.createTable(this.STATUS_TABLE, (table) => {
table.string('id', 36).primary();
table.string('tenant_id', 36).notNullable().index();
table.string('order_id', 36).notNullable().index();
table.enum('status', ['pending', 'picked', 'packed', 'shipped', 'delivered']).defaultTo('pending');
table.string('tracking_number', 64);
table.string('carrier', 64);
table.json('events');
table.datetime('created_at').notNullable().defaultTo(db.fn.now());
table.datetime('updated_at').notNullable().defaultTo(db.fn.now());
});
logger.info(`[UnifiedFulfillment] Table ${this.STATUS_TABLE} created`);
}
}
static async unifyOrders(
tenantId: string,
platformOrders: Array<{
platform: string;
orderId: string;
data: Record<string, any>;
}>
): Promise<UnifiedOrder[]> {
const unified: UnifiedOrder[] = [];
for (const platformOrder of platformOrders) {
const id = this.generateId();
const order: UnifiedOrder = {
id,
tenantId,
platformOrderId: platformOrder.orderId,
platform: platformOrder.platform,
customerId: platformOrder.data.customerId || `cust_${Date.now()}`,
items: platformOrder.data.items || [],
totalAmount: platformOrder.data.totalAmount || 0,
currency: platformOrder.data.currency || 'USD',
status: 'pending',
shippingAddress: platformOrder.data.shippingAddress || {},
fulfillmentStatus: 'unfulfilled',
createdAt: new Date(),
updatedAt: new Date(),
};
await db(this.ORDER_TABLE).insert({
id: order.id,
tenant_id: order.tenantId,
platform_order_id: order.platformOrderId,
platform: order.platform,
customer_id: order.customerId,
items: JSON.stringify(order.items),
total_amount: order.totalAmount,
currency: order.currency,
status: order.status,
shipping_address: JSON.stringify(order.shippingAddress),
fulfillment_status: order.fulfillmentStatus,
created_at: order.createdAt,
updated_at: order.updatedAt,
});
unified.push(order);
}
logger.info(`[UnifiedFulfillment] Unified ${unified.length} orders from ${platformOrders.length} platforms`);
return unified;
}
static async routeOrder(
tenantId: string,
orderId: string,
inventoryData: Array<{ warehouseId: string; available: number }>
): Promise<FulfillmentRoute> {
const order = await db(this.ORDER_TABLE)
.where({ tenant_id: tenantId, id: orderId })
.first();
if (!order) {
throw new Error(`Order ${orderId} not found`);
}
const items: OrderItem[] = JSON.parse(order.items);
const totalQuantity = items.reduce((sum, item) => sum + item.quantity, 0);
let bestWarehouse = inventoryData[0]?.warehouseId || 'default_warehouse';
let maxAvailable = 0;
for (const inv of inventoryData) {
if (inv.available >= totalQuantity && inv.available > maxAvailable) {
maxAvailable = inv.available;
bestWarehouse = inv.warehouseId;
}
}
const route: FulfillmentRoute = {
orderId,
warehouseId: bestWarehouse,
priority: 0,
estimatedShipDate: new Date(Date.now() + 24 * 60 * 60 * 1000),
estimatedDeliveryDate: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
cost: 5.99,
};
await db(this.ROUTE_TABLE).insert({
id: this.generateId(),
tenant_id: tenantId,
order_id: orderId,
warehouse_id: route.warehouseId,
priority: route.priority,
estimated_ship_date: route.estimatedShipDate,
estimated_delivery_date: route.estimatedDeliveryDate,
cost: route.cost,
created_at: new Date(),
});
await DomainEventBus.publish({
type: 'order.routed',
tenantId,
data: { orderId, warehouseId: bestWarehouse },
timestamp: new Date(),
});
logger.info(`[UnifiedFulfillment] Routed order ${orderId} to warehouse ${bestWarehouse}`);
return route;
}
static async manageFulfillment(
tenantId: string,
orderId: string,
action: 'pick' | 'pack' | 'ship' | 'deliver',
params: Record<string, any>
): Promise<FulfillmentStatus> {
let status = await db(this.STATUS_TABLE)
.where({ tenant_id: tenantId, order_id: orderId })
.first();
const statusMap: Record<string, FulfillmentStatus['status']> = {
pick: 'picked',
pack: 'packed',
ship: 'shipped',
deliver: 'delivered',
};
const newStatus = statusMap[action];
const event: FulfillmentEvent = {
timestamp: new Date(),
status: newStatus,
location: params.location,
description: `Order ${action}ed`,
};
if (status) {
const events: FulfillmentEvent[] = status.events ? JSON.parse(status.events) : [];
events.push(event);
await db(this.STATUS_TABLE)
.where({ id: status.id })
.update({
status: newStatus,
tracking_number: params.trackingNumber || status.tracking_number,
carrier: params.carrier || status.carrier,
events: JSON.stringify(events),
updated_at: new Date(),
});
} else {
status = {
id: this.generateId(),
tenant_id: tenantId,
order_id: orderId,
status: newStatus,
tracking_number: params.trackingNumber,
carrier: params.carrier,
events: JSON.stringify([event]),
created_at: new Date(),
updated_at: new Date(),
} as any;
await db(this.STATUS_TABLE).insert(status);
}
await db(this.ORDER_TABLE)
.where({ tenant_id: tenantId, id: orderId })
.update({
status: newStatus === 'delivered' ? 'delivered' : 'processing',
fulfillment_status: newStatus === 'delivered' ? 'fulfilled' : 'partial',
updated_at: new Date(),
});
logger.info(`[UnifiedFulfillment] Updated fulfillment status for order ${orderId} to ${newStatus}`);
return this.rowToStatus(status);
}
static async syncStatus(
tenantId: string,
orderId: string,
newStatus: string
): Promise<{ success: boolean; message: string }> {
const order = await db(this.ORDER_TABLE)
.where({ tenant_id: tenantId, id: orderId })
.first();
if (!order) {
return { success: false, message: 'Order not found' };
}
await db(this.ORDER_TABLE)
.where({ tenant_id: tenantId, id: orderId })
.update({
status: newStatus,
updated_at: new Date(),
});
await DomainEventBus.publish({
type: 'order.status.synced',
tenantId,
data: { orderId, newStatus, platform: order.platform },
timestamp: new Date(),
});
logger.info(`[UnifiedFulfillment] Synced order ${orderId} status to ${newStatus}`);
return { success: true, message: `Status synced to ${newStatus}` };
}
private static rowToStatus(row: any): FulfillmentStatus {
return {
orderId: row.order_id,
status: row.status,
trackingNumber: row.tracking_number,
carrier: row.carrier,
events: row.events ? JSON.parse(row.events) : [],
};
}
private static generateId(): string {
return `ufo_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
}

View File

@@ -0,0 +1,193 @@
import db from '../config/database';
import { DomainEventBus } from '../core/runtime/DomainEventBus';
import { logger } from '../utils/logger';
export interface WinNode {
id: string;
tenantId: string;
name: string;
host: string;
port: number;
status: 'ONLINE' | 'OFFLINE' | 'BUSY' | 'ERROR';
shopId: string;
shopName: string;
profileDir: string;
proxy: string;
fingerprintPolicy: 'STANDARD' | 'STEALTH' | 'RANDOM' | 'CUSTOM';
maxConcurrent: number;
currentTasks: number;
cpuUsage: number;
memoryUsage: number;
lastHeartbeat: string;
autoRestart: boolean;
createdAt: string;
updatedAt: string;
}
export interface WinNodeHeartbeat {
nodeId: string;
cpuUsage: number;
memoryUsage: number;
currentTasks: number;
timestamp: string;
}
export class WinNodeService {
private static TABLE = 'cf_win_node';
static async initTable(): Promise<void> {
const hasTable = await db.schema.hasTable(this.TABLE);
if (!hasTable) {
await db.schema.createTable(this.TABLE, (table) => {
table.string('id', 64).primary();
table.string('tenant_id', 64).notNullable();
table.string('name', 128).notNullable();
table.string('host', 64).notNullable();
table.integer('port').notNullable();
table.enum('status', ['ONLINE', 'OFFLINE', 'BUSY', 'ERROR']).defaultTo('OFFLINE');
table.string('shop_id', 64);
table.string('shop_name', 128);
table.string('profile_dir', 256).notNullable();
table.string('proxy', 256);
table.enum('fingerprint_policy', ['STANDARD', 'STEALTH', 'RANDOM', 'CUSTOM']).defaultTo('STANDARD');
table.integer('max_concurrent').defaultTo(3);
table.integer('current_tasks').defaultTo(0);
table.decimal('cpu_usage', 5, 2).defaultTo(0);
table.decimal('memory_usage', 5, 2).defaultTo(0);
table.timestamp('last_heartbeat');
table.boolean('auto_restart').defaultTo(true);
table.timestamps(true, true);
table.index(['tenant_id', 'status']);
table.index(['shop_id']);
});
}
}
static async list(tenantId: string, filters?: { status?: string; shopId?: string }): Promise<WinNode[]> {
let query = db(this.TABLE).where('tenant_id', tenantId);
if (filters?.status) query = query.where('status', filters.status);
if (filters?.shopId) query = query.where('shop_id', filters.shopId);
return query.orderBy('created_at', 'desc');
}
static async getById(id: string): Promise<WinNode | null> {
const node = await db(this.TABLE).where({ id }).first();
return node || null;
}
static async create(tenantId: string, data: Omit<WinNode, 'id' | 'tenantId' | 'createdAt' | 'updatedAt' | 'currentTasks' | 'cpuUsage' | 'memoryUsage' | 'lastHeartbeat'>): Promise<WinNode> {
const id = this.generateId();
const now = new Date().toISOString();
const node: WinNode = {
id,
tenantId,
...data,
currentTasks: 0,
cpuUsage: 0,
memoryUsage: 0,
lastHeartbeat: now,
createdAt: now,
updatedAt: now,
};
await db(this.TABLE).insert({
id: node.id,
tenant_id: node.tenantId,
name: node.name,
host: node.host,
port: node.port,
status: node.status,
shop_id: node.shopId,
shop_name: node.shopName,
profile_dir: node.profileDir,
proxy: node.proxy,
fingerprint_policy: node.fingerprintPolicy,
max_concurrent: node.maxConcurrent,
current_tasks: node.currentTasks,
cpu_usage: node.cpuUsage,
memory_usage: node.memoryUsage,
last_heartbeat: node.lastHeartbeat,
auto_restart: node.autoRestart,
created_at: node.createdAt,
updated_at: node.updatedAt,
});
await DomainEventBus.publish('win_node.created', { nodeId: id, tenantId });
return node;
}
static async update(id: string, data: Partial<WinNode>): Promise<WinNode> {
const updateData: Record<string, any> = { updated_at: new Date().toISOString() };
if (data.name) updateData.name = data.name;
if (data.host) updateData.host = data.host;
if (data.port) updateData.port = data.port;
if (data.status) updateData.status = data.status;
if (data.shopId !== undefined) updateData.shop_id = data.shopId;
if (data.shopName) updateData.shop_name = data.shopName;
if (data.profileDir) updateData.profile_dir = data.profileDir;
if (data.proxy) updateData.proxy = data.proxy;
if (data.fingerprintPolicy) updateData.fingerprint_policy = data.fingerprintPolicy;
if (data.maxConcurrent !== undefined) updateData.max_concurrent = data.maxConcurrent;
if (data.autoRestart !== undefined) updateData.auto_restart = data.autoRestart;
await db(this.TABLE).where({ id }).update(updateData);
const node = await this.getById(id);
await DomainEventBus.publish('win_node.updated', { nodeId: id });
return node!;
}
static async delete(id: string): Promise<void> {
await db(this.TABLE).where({ id }).delete();
await DomainEventBus.publish('win_node.deleted', { nodeId: id });
}
static async heartbeat(nodeId: string, data: WinNodeHeartbeat): Promise<void> {
await db(this.TABLE).where({ id: nodeId }).update({
cpu_usage: data.cpuUsage,
memory_usage: data.memoryUsage,
current_tasks: data.currentTasks,
last_heartbeat: data.timestamp,
status: data.currentTasks > 0 ? 'BUSY' : 'ONLINE',
updated_at: new Date().toISOString(),
});
}
static async testConnection(id: string): Promise<{ success: boolean; message: string }> {
const node = await this.getById(id);
if (!node) return { success: false, message: 'Node not found' };
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000);
const response = await fetch(`http://${node.host}:${node.port}/json/version`, {
signal: controller.signal,
});
clearTimeout(timeoutId);
if (response.ok) {
await db(this.TABLE).where({ id }).update({ status: 'ONLINE', updated_at: new Date().toISOString() });
return { success: true, message: 'Connection successful' };
}
return { success: false, message: `HTTP ${response.status}` };
} catch (error: any) {
await db(this.TABLE).where({ id }).update({ status: 'ERROR', updated_at: new Date().toISOString() });
return { success: false, message: error.message };
}
}
static async restart(id: string): Promise<{ success: boolean; message: string }> {
const node = await this.getById(id);
if (!node) return { success: false, message: 'Node not found' };
await DomainEventBus.publish('win_node.restart_requested', { nodeId: id, shopId: node.shopId });
return { success: true, message: 'Restart command sent' };
}
static async getStats(tenantId: string): Promise<{ total: number; online: number; busy: number; offline: number }> {
const nodes = await this.list(tenantId);
return {
total: nodes.length,
online: nodes.filter((n) => n.status === 'ONLINE').length,
busy: nodes.filter((n) => n.status === 'BUSY').length,
offline: nodes.filter((n) => n.status === 'OFFLINE' || n.status === 'ERROR').length,
};
}
private static generateId(): string {
return `wn_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
}