Files
makemd/server/src/services/OmnichannelMarketingService.ts
wurenzhi 48a78137c5 refactor(terminology): 统一术语标准并优化代码类型安全
- 将B2B统一为TOB术语
- 将状态值统一为大写格式
- 优化类型声明,避免使用any
- 将float类型替换为decimal以提高精度
- 新增术语标准化文档
- 优化路由结构和菜单分类
- 添加TypeORM实体类
- 增强加密模块安全性
- 重构前端路由结构
- 完善任务模板和验收标准
2026-03-20 09:43:50 +08:00

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)}`;
}
}