- 将B2B统一为TOB术语 - 将状态值统一为大写格式 - 优化类型声明,避免使用any - 将float类型替换为decimal以提高精度 - 新增术语标准化文档 - 优化路由结构和菜单分类 - 添加TypeORM实体类 - 增强加密模块安全性 - 重构前端路由结构 - 完善任务模板和验收标准
290 lines
9.1 KiB
TypeScript
290 lines
9.1 KiB
TypeScript
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)}`;
|
|
}
|
|
}
|