客户关系管理系统
本文介绍如何使用 Entity Engine 构建一个功能完整的客户关系管理(CRM)系统,涵盖客户管理、销售管道、营销活动、服务支持和业务分析等核心功能。
系统架构
核心实体模型
// 客户模型
export const CustomerModel: IEntityModel = {
name: 'Customer',
fields: {
id: { type: 'uuid', primaryKey: true },
customerNumber: { type: 'string', unique: true, required: true },
type: {
type: 'enum',
enumValues: ['INDIVIDUAL', 'COMPANY'],
defaultValue: 'INDIVIDUAL'
},
// 个人信息
firstName: { type: 'string' },
lastName: { type: 'string' },
fullName: { type: 'string' },
title: { type: 'string' },
// 公司信息
companyName: { type: 'string' },
industry: { type: 'string' },
employeeCount: { type: 'number' },
annualRevenue: { type: 'decimal' },
// 联系信息
email: { type: 'string', required: true },
phone: { type: 'string' },
mobile: { type: 'string' },
website: { type: 'string' },
// 地址信息
addresses: {
type: 'relation',
relation: { type: 'one_to_many', model: 'CustomerAddress', foreignKey: 'customerId' }
},
// 状态和分类
status: {
type: 'enum',
enumValues: ['LEAD', 'PROSPECT', 'CUSTOMER', 'INACTIVE'],
defaultValue: 'LEAD'
},
priority: {
type: 'enum',
enumValues: ['LOW', 'MEDIUM', 'HIGH', 'CRITICAL'],
defaultValue: 'MEDIUM'
},
source: { type: 'string' }, // 客户来源
tags: { type: 'json', defaultValue: [] },
// 关系管理
assignedToId: { type: 'uuid' },
assignedTo: {
type: 'relation',
relation: { type: 'many_to_one', model: 'User', targetKey: 'id' }
},
teamId: { type: 'uuid' },
team: {
type: 'relation',
relation: { type: 'many_to_one', model: 'Team', targetKey: 'id' }
},
// 业务数据
opportunities: {
type: 'relation',
relation: { type: 'one_to_many', model: 'Opportunity', foreignKey: 'customerId' }
},
activities: {
type: 'relation',
relation: { type: 'one_to_many', model: 'Activity', foreignKey: 'customerId' }
},
tickets: {
type: 'relation',
relation: { type: 'one_to_many', model: 'Ticket', foreignKey: 'customerId' }
},
invoices: {
type: 'relation',
relation: { type: 'one_to_many', model: 'Invoice', foreignKey: 'customerId' }
},
// 统计数据
totalValue: { type: 'decimal', defaultValue: 0 },
lastContactDate: { type: 'datetime' },
nextContactDate: { type: 'datetime' },
lifetimeValue: { type: 'decimal', defaultValue: 0 },
// 自定义字段
customFields: { type: 'json', defaultValue: {} },
notes: { type: 'text' },
// 时间戳
createdAt: { type: 'datetime', defaultValue: 'now' },
updatedAt: { type: 'datetime', defaultValue: 'now' }
},
indexes: [
{ fields: ['customerNumber'], unique: true },
{ fields: ['email'] },
{ fields: ['status'] },
{ fields: ['assignedToId'] },
{ fields: ['createdAt'] }
]
};
// 销售机会模型
export const OpportunityModel: IEntityModel = {
name: 'Opportunity',
fields: {
id: { type: 'uuid', primaryKey: true },
title: { type: 'string', required: true },
description: { type: 'text' },
customerId: { type: 'uuid', required: true },
customer: {
type: 'relation',
relation: { type: 'many_to_one', model: 'Customer', targetKey: 'id' }
},
// 销售信息
amount: { type: 'decimal', required: true },
currency: { type: 'string', defaultValue: 'USD' },
probability: { type: 'number', min: 0, max: 100, defaultValue: 50 },
expectedCloseDate: { type: 'date' },
actualCloseDate: { type: 'date' },
// 阶段管理
stageId: { type: 'uuid', required: true },
stage: {
type: 'relation',
relation: { type: 'many_to_one', model: 'OpportunityStage', targetKey: 'id' }
},
status: {
type: 'enum',
enumValues: ['OPEN', 'WON', 'LOST', 'CANCELLED'],
defaultValue: 'OPEN'
},
lostReason: { type: 'string' },
// 负责人
ownerId: { type: 'uuid', required: true },
owner: {
type: 'relation',
relation: { type: 'many_to_one', model: 'User', targetKey: 'id' }
},
teamId: { type: 'uuid' },
team: {
type: 'relation',
relation: { type: 'many_to_one', model: 'Team', targetKey: 'id' }
},
// 产品和服务
products: {
type: 'relation',
relation: { type: 'many_to_many', model: 'Product', through: 'OpportunityProduct' }
},
// 活动记录
activities: {
type: 'relation',
relation: { type: 'one_to_many', model: 'Activity', foreignKey: 'opportunityId' }
},
// 竞争对手
competitors: { type: 'json', defaultValue: [] },
// 自定义字段
customFields: { type: 'json', defaultValue: {} },
// 时间戳
createdAt: { type: 'datetime', defaultValue: 'now' },
updatedAt: { type: 'datetime', defaultValue: 'now' }
},
indexes: [
{ fields: ['customerId'] },
{ fields: ['stageId'] },
{ fields: ['ownerId'] },
{ fields: ['status'] },
{ fields: ['expectedCloseDate'] },
{ fields: ['createdAt'] }
]
};
// 销售阶段模型
export const OpportunityStageModel: IEntityModel = {
name: 'OpportunityStage',
fields: {
id: { type: 'uuid', primaryKey: true },
name: { type: 'string', required: true },
description: { type: 'string' },
probability: { type: 'number', min: 0, max: 100 },
sortOrder: { type: 'number', required: true },
color: { type: 'string' },
isActive: { type: 'boolean', defaultValue: true },
isWon: { type: 'boolean', defaultValue: false },
isLost: { type: 'boolean', defaultValue: false },
requirements: { type: 'json', defaultValue: [] }, // 进入此阶段的要求
opportunities: {
type: 'relation',
relation: { type: 'one_to_many', model: 'Opportunity', foreignKey: 'stageId' }
},
createdAt: { type: 'datetime', defaultValue: 'now' },
updatedAt: { type: 'datetime', defaultValue: 'now' }
},
indexes: [
{ fields: ['sortOrder'] },
{ fields: ['isActive'] }
]
};
// 活动模型
export const ActivityModel: IEntityModel = {
name: 'Activity',
fields: {
id: { type: 'uuid', primaryKey: true },
type: {
type: 'enum',
enumValues: ['CALL', 'EMAIL', 'MEETING', 'TASK', 'NOTE', 'SMS', 'DEMO'],
required: true
},
subject: { type: 'string', required: true },
description: { type: 'text' },
// 关联对象
customerId: { type: 'uuid' },
customer: {
type: 'relation',
relation: { type: 'many_to_one', model: 'Customer', targetKey: 'id' }
},
opportunityId: { type: 'uuid' },
opportunity: {
type: 'relation',
relation: { type: 'many_to_one', model: 'Opportunity', targetKey: 'id' }
},
ticketId: { type: 'uuid' },
ticket: {
type: 'relation',
relation: { type: 'many_to_one', model: 'Ticket', targetKey: 'id' }
},
// 时间管理
startDate: { type: 'datetime', required: true },
endDate: { type: 'datetime' },
duration: { type: 'number' }, // 分钟
isAllDay: { type: 'boolean', defaultValue: false },
// 状态
status: {
type: 'enum',
enumValues: ['PLANNED', 'IN_PROGRESS', 'COMPLETED', 'CANCELLED'],
defaultValue: 'PLANNED'
},
priority: {
type: 'enum',
enumValues: ['LOW', 'NORMAL', 'HIGH', 'URGENT'],
defaultValue: 'NORMAL'
},
// 负责人和参与者
ownerId: { type: 'uuid', required: true },
owner: {
type: 'relation',
relation: { type: 'many_to_one', model: 'User', targetKey: 'id' }
},
participants: {
type: 'relation',
relation: { type: 'many_to_many', model: 'User', through: 'ActivityParticipant' }
},
// 结果记录
outcome: { type: 'string' },
nextSteps: { type: 'text' },
followUpDate: { type: 'datetime' },
// 附件和链接
attachments: { type: 'json', defaultValue: [] },
links: { type: 'json', defaultValue: [] },
// 自定义字段
customFields: { type: 'json', defaultValue: {} },
// 时间戳
createdAt: { type: 'datetime', defaultValue: 'now' },
updatedAt: { type: 'datetime', defaultValue: 'now' }
},
indexes: [
{ fields: ['customerId'] },
{ fields: ['opportunityId'] },
{ fields: ['ownerId'] },
{ fields: ['type'] },
{ fields: ['status'] },
{ fields: ['startDate'] },
{ fields: ['createdAt'] }
]
};
// 营销活动模型
export const CampaignModel: IEntityModel = {
name: 'Campaign',
fields: {
id: { type: 'uuid', primaryKey: true },
name: { type: 'string', required: true },
description: { type: 'text' },
type: {
type: 'enum',
enumValues: ['EMAIL', 'SMS', 'SOCIAL', 'WEBINAR', 'TRADE_SHOW', 'DIRECT_MAIL', 'OTHER'],
required: true
},
status: {
type: 'enum',
enumValues: ['PLANNING', 'ACTIVE', 'PAUSED', 'COMPLETED', 'CANCELLED'],
defaultValue: 'PLANNING'
},
// 时间安排
startDate: { type: 'datetime' },
endDate: { type: 'datetime' },
// 预算和成本
budget: { type: 'decimal' },
actualCost: { type: 'decimal', defaultValue: 0 },
// 目标设置
targetAudience: { type: 'string' },
expectedRevenue: { type: 'decimal' },
expectedLeads: { type: 'number' },
// 内容
message: { type: 'text' },
creativeAssets: { type: 'json', defaultValue: [] },
// 渠道配置
channels: { type: 'json', defaultValue: [] },
// 负责人
ownerId: { type: 'uuid', required: true },
owner: {
type: 'relation',
relation: { type: 'many_to_one', model: 'User', targetKey: 'id' }
},
teamId: { type: 'uuid' },
team: {
type: 'relation',
relation: { type: 'many_to_one', model: 'Team', targetKey: 'id' }
},
// 目标客户
targets: {
type: 'relation',
relation: { type: 'many_to_many', model: 'Customer', through: 'CampaignTarget' }
},
// 生成的线索
leads: {
type: 'relation',
relation: { type: 'one_to_many', model: 'Customer', foreignKey: 'sourceId' }
},
// 统计数据
sentCount: { type: 'number', defaultValue: 0 },
deliveredCount: { type: 'number', defaultValue: 0 },
openCount: { type: 'number', defaultValue: 0 },
clickCount: { type: 'number', defaultValue: 0 },
responseCount: { type: 'number', defaultValue: 0 },
conversionCount: { type: 'number', defaultValue: 0 },
// 自定义字段
customFields: { type: 'json', defaultValue: {} },
// 时间戳
createdAt: { type: 'datetime', defaultValue: 'now' },
updatedAt: { type: 'datetime', defaultValue: 'now' }
},
indexes: [
{ fields: ['status'] },
{ fields: ['type'] },
{ fields: ['ownerId'] },
{ fields: ['startDate'] },
{ fields: ['createdAt'] }
]
};
// 服务工单模型
export const TicketModel: IEntityModel = {
name: 'Ticket',
fields: {
id: { type: 'uuid', primaryKey: true },
ticketNumber: { type: 'string', unique: true, required: true },
subject: { type: 'string', required: true },
description: { type: 'text', required: true },
customerId: { type: 'uuid', required: true },
customer: {
type: 'relation',
relation: { type: 'many_to_one', model: 'Customer', targetKey: 'id' }
},
// 分类
category: { type: 'string' },
subcategory: { type: 'string' },
type: {
type: 'enum',
enumValues: ['QUESTION', 'PROBLEM', 'REQUEST', 'INCIDENT'],
required: true
},
// 优先级和状态
priority: {
type: 'enum',
enumValues: ['LOW', 'NORMAL', 'HIGH', 'URGENT'],
defaultValue: 'NORMAL'
},
status: {
type: 'enum',
enumValues: ['NEW', 'OPEN', 'PENDING', 'ESCALATED', 'RESOLVED', 'CLOSED'],
defaultValue: 'NEW'
},
resolution: { type: 'text' },
// 负责人
assignedToId: { type: 'uuid' },
assignedTo: {
type: 'relation',
relation: { type: 'many_to_one', model: 'User', targetKey: 'id' }
},
teamId: { type: 'uuid' },
team: {
type: 'relation',
relation: { type: 'many_to_one', model: 'Team', targetKey: 'id' }
},
// 时间管理
createdAt: { type: 'datetime', defaultValue: 'now' },
updatedAt: { type: 'datetime', defaultValue: 'now' },
firstResponseAt: { type: 'datetime' },
resolvedAt: { type: 'datetime' },
closedAt: { type: 'datetime' },
dueDate: { type: 'datetime' },
// SLA指标
responseTime: { type: 'number' }, // 分钟
resolutionTime: { type: 'number' }, // 分钟
escalationLevel: { type: 'number', defaultValue: 0 },
// 沟通记录
communications: {
type: 'relation',
relation: { type: 'one_to_many', model: 'TicketCommunication', foreignKey: 'ticketId' }
},
// 关联信息
relatedTickets: { type: 'json', defaultValue: [] },
tags: { type: 'json', defaultValue: [] },
// 满意度
satisfactionRating: { type: 'number', min: 1, max: 5 },
satisfactionComment: { type: 'text' },
// 自定义字段
customFields: { type: 'json', defaultValue: {} }
},
indexes: [
{ fields: ['ticketNumber'], unique: true },
{ fields: ['customerId'] },
{ fields: ['status'] },
{ fields: ['priority'] },
{ fields: ['assignedToId'] },
{ fields: ['createdAt'] }
]
};
// 产品模型
export const ProductModel: IEntityModel = {
name: 'Product',
fields: {
id: { type: 'uuid', primaryKey: true },
name: { type: 'string', required: true },
code: { type: 'string', unique: true, required: true },
description: { type: 'text' },
category: { type: 'string' },
// 定价
price: { type: 'decimal', required: true },
cost: { type: 'decimal' },
currency: { type: 'string', defaultValue: 'USD' },
// 状态
status: {
type: 'enum',
enumValues: ['ACTIVE', 'INACTIVE', 'DISCONTINUED'],
defaultValue: 'ACTIVE'
},
// 销售信息
opportunities: {
type: 'relation',
relation: { type: 'many_to_many', model: 'Opportunity', through: 'OpportunityProduct' }
},
// 自定义字段
customFields: { type: 'json', defaultValue: {} },
// 时间戳
createdAt: { type: 'datetime', defaultValue: 'now' },
updatedAt: { type: 'datetime', defaultValue: 'now' }
},
indexes: [
{ fields: ['code'], unique: true },
{ fields: ['status'] },
{ fields: ['category'] }
]
};
// 团队模型
export const TeamModel: IEntityModel = {
name: 'Team',
fields: {
id: { type: 'uuid', primaryKey: true },
name: { type: 'string', required: true },
description: { type: 'string' },
type: {
type: 'enum',
enumValues: ['SALES', 'MARKETING', 'SUPPORT', 'MANAGEMENT'],
required: true
},
managerId: { type: 'uuid' },
manager: {
type: 'relation',
relation: { type: 'many_to_one', model: 'User', targetKey: 'id' }
},
members: {
type: 'relation',
relation: { type: 'many_to_many', model: 'User', through: 'TeamMember' }
},
customers: {
type: 'relation',
relation: { type: 'one_to_many', model: 'Customer', foreignKey: 'teamId' }
},
isActive: { type: 'boolean', defaultValue: true },
createdAt: { type: 'datetime', defaultValue: 'now' },
updatedAt: { type: 'datetime', defaultValue: 'now' }
},
indexes: [
{ fields: ['type'] },
{ fields: ['managerId'] },
{ fields: ['isActive'] }
]
};
业务逻辑实现
客户管理动作处理器
// 客户管理动作处理器
class CustomerManagerActionHandler implements IActionHandler {
name = 'customer-manager';
displayName = '客户管理';
category = ActionCategory.BUSINESS;
inputSchema: ActionInputSchema = {
type: 'object',
properties: {
action: {
type: 'string',
enum: ['create', 'update', 'merge', 'convert', 'assign', 'segment'],
required: true
},
customerId: { type: 'string' },
data: { type: 'object' },
options: { type: 'object' }
}
};
async execute(context: ActionExecutionContext): Promise<ActionResult> {
const { input, entityManager, user } = context;
try {
switch (input.action) {
case 'create':
return await this.createCustomer(input, entityManager, user);
case 'update':
return await this.updateCustomer(input, entityManager, user);
case 'merge':
return await this.mergeCustomers(input, entityManager, user);
case 'convert':
return await this.convertLead(input, entityManager, user);
case 'assign':
return await this.assignCustomer(input, entityManager, user);
case 'segment':
return await this.segmentCustomers(input, entityManager, user);
default:
throw new Error(`Unknown customer action: ${input.action}`);
}
} catch (error) {
return {
success: false,
error: { message: error.message, code: 'CUSTOMER_ACTION_ERROR' }
};
}
}
private async createCustomer(input: any, entityManager: IEntityManager, user: any): Promise<ActionResult> {
// 验证邮箱唯一性
const existingCustomer = await entityManager.query({
entityType: 'Customer',
filter: { email: input.data.email },
limit: 1
});
if (existingCustomer.data.length > 0) {
throw new Error('Customer with this email already exists');
}
// 生成客户编号
const customerNumber = await this.generateCustomerNumber(entityManager);
// 构建全名
const fullName = input.data.type === 'INDIVIDUAL' ?
`${input.data.firstName} ${input.data.lastName}`.trim() :
input.data.companyName;
// 创建客户
const customer = await entityManager.create('Customer', {
customerNumber,
type: input.data.type || 'INDIVIDUAL',
firstName: input.data.firstName,
lastName: input.data.lastName,
fullName,
title: input.data.title,
companyName: input.data.companyName,
industry: input.data.industry,
email: input.data.email,
phone: input.data.phone,
mobile: input.data.mobile,
website: input.data.website,
status: input.data.status || 'LEAD',
priority: input.data.priority || 'MEDIUM',
source: input.data.source,
tags: input.data.tags || [],
assignedToId: input.data.assignedToId || user.id,
teamId: input.data.teamId,
customFields: input.data.customFields || {},
notes: input.data.notes
});
// 创建地址信息
if (input.data.addresses && input.data.addresses.length > 0) {
for (const addressData of input.data.addresses) {
await entityManager.create('CustomerAddress', {
customerId: customer.id,
type: addressData.type,
street: addressData.street,
city: addressData.city,
state: addressData.state,
zipCode: addressData.zipCode,
country: addressData.country,
isPrimary: addressData.isPrimary || false
});
}
}
// 创建初始活动记录
await entityManager.create('Activity', {
type: 'NOTE',
subject: 'Customer created',
description: `New ${customer.type.toLowerCase()} customer created: ${customer.fullName}`,
customerId: customer.id,
startDate: new Date(),
status: 'COMPLETED',
ownerId: user.id
});
return {
success: true,
data: { customer },
events: [{
type: 'customer.created',
data: {
customerId: customer.id,
customerNumber: customer.customerNumber,
type: customer.type,
createdBy: user.id
}
}]
};
}
private async convertLead(input: any, entityManager: IEntityManager, user: any): Promise<ActionResult> {
const customer = await entityManager.get('Customer', input.customerId);
if (!customer) {
throw new Error('Customer not found');
}
if (customer.status !== 'LEAD') {
throw new Error('Only leads can be converted');
}
// 更新客户状态
const updatedCustomer = await entityManager.update('Customer', input.customerId, {
status: 'CUSTOMER',
updatedAt: new Date()
});
// 创建转换记录
await entityManager.create('Activity', {
type: 'NOTE',
subject: 'Lead converted to customer',
description: `Lead ${customer.fullName} has been converted to a customer`,
customerId: customer.id,
startDate: new Date(),
status: 'COMPLETED',
ownerId: user.id
});
// 如果提供了销售机会信息,创建销售机会
if (input.data.opportunity) {
const opportunity = await entityManager.create('Opportunity', {
title: input.data.opportunity.title,
description: input.data.opportunity.description,
customerId: customer.id,
amount: input.data.opportunity.amount,
probability: input.data.opportunity.probability || 25,
expectedCloseDate: input.data.opportunity.expectedCloseDate,
stageId: input.data.opportunity.stageId,
ownerId: user.id
});
return {
success: true,
data: { customer: updatedCustomer, opportunity },
events: [{
type: 'customer.converted',
data: {
customerId: customer.id,
opportunityId: opportunity.id,
convertedBy: user.id
}
}]
};
}
return {
success: true,
data: { customer: updatedCustomer },
events: [{
type: 'customer.converted',
data: {
customerId: customer.id,
convertedBy: user.id
}
}]
};
}
private async mergeCustomers(input: any, entityManager: IEntityManager, user: any): Promise<ActionResult> {
const primaryCustomerId = input.data.primaryCustomerId;
const secondaryCustomerId = input.data.secondaryCustomerId;
const primaryCustomer = await entityManager.get('Customer', primaryCustomerId);
const secondaryCustomer = await entityManager.get('Customer', secondaryCustomerId);
if (!primaryCustomer || !secondaryCustomer) {
throw new Error('One or both customers not found');
}
// 在事务中执行合并操作
return await context.transaction.run(async () => {
// 合并客户数据
const mergedData = this.mergeCustomerData(primaryCustomer, secondaryCustomer, input.data.mergeRules);
// 更新主客户
const updatedCustomer = await entityManager.update('Customer', primaryCustomerId, mergedData);
// 迁移关联数据
await this.migrateRelatedData(secondaryCustomerId, primaryCustomerId, entityManager);
// 标记次要客户为已合并
await entityManager.update('Customer', secondaryCustomerId, {
status: 'INACTIVE',
notes: `Merged into customer ${primaryCustomer.customerNumber} on ${new Date().toISOString()}`
});
// 创建合并记录
await entityManager.create('Activity', {
type: 'NOTE',
subject: 'Customer merge completed',
description: `Customer ${secondaryCustomer.fullName} (${secondaryCustomer.customerNumber}) merged into ${primaryCustomer.fullName} (${primaryCustomer.customerNumber})`,
customerId: primaryCustomerId,
startDate: new Date(),
status: 'COMPLETED',
ownerId: user.id
});
return {
success: true,
data: { customer: updatedCustomer },
events: [{
type: 'customer.merged',
data: {
primaryCustomerId,
secondaryCustomerId,
mergedBy: user.id
}
}]
};
});
}
private async generateCustomerNumber(entityManager: IEntityManager): Promise<string> {
// 获取当前年份
const year = new Date().getFullYear();
// 获取今年创建的客户数量
const count = await entityManager.query({
entityType: 'Customer',
filter: {
createdAt: {
$gte: new Date(year, 0, 1),
$lt: new Date(year + 1, 0, 1)
}
},
operation: 'count'
});
// 生成格式:CUST-YYYY-NNNNNN
const sequence = (count.total + 1).toString().padStart(6, '0');
return `CUST-${year}-${sequence}`;
}
private mergeCustomerData(primary: any, secondary: any, mergeRules: any): any {
const merged = { ...primary };
// 根据合并规则合并字段
for (const [field, rule] of Object.entries(mergeRules || {})) {
switch (rule) {
case 'primary':
// 保持主客户的值
break;
case 'secondary':
if (secondary[field]) {
merged[field] = secondary[field];
}
break;
case 'combine':
if (field === 'tags') {
merged[field] = [...new Set([...primary[field], ...secondary[field]])];
} else if (field === 'notes') {
merged[field] = [primary[field], secondary[field]].filter(Boolean).join('\n\n---\n\n');
}
break;
case 'latest':
if (secondary.updatedAt > primary.updatedAt && secondary[field]) {
merged[field] = secondary[field];
}
break;
}
}
// 合并自定义字段
merged.customFields = { ...primary.customFields, ...secondary.customFields };
return merged;
}
private async migrateRelatedData(fromId: string, toId: string, entityManager: IEntityManager): Promise<void> {
// 迁移销售机会
await entityManager.query({
entityType: 'Opportunity',
filter: { customerId: fromId },
operation: 'update',
data: { customerId: toId }
});
// 迁移活动记录
await entityManager.query({
entityType: 'Activity',
filter: { customerId: fromId },
operation: 'update',
data: { customerId: toId }
});
// 迁移工单
await entityManager.query({
entityType: 'Ticket',
filter: { customerId: fromId },
operation: 'update',
data: { customerId: toId }
});
// 迁移发票
await entityManager.query({
entityType: 'Invoice',
filter: { customerId: fromId },
operation: 'update',
data: { customerId: toId }
});
}
async validate(input: any): Promise<ValidationResult> {
const errors: string[] = [];
if (!input.action) {
errors.push('action is required');
}
if (input.action === 'create' && !input.data) {
errors.push('data is required for create action');
}
if (input.action === 'create' && !input.data.email) {
errors.push('email is required for create action');
}
if (['update', 'convert', 'assign'].includes(input.action) && !input.customerId) {
errors.push('customerId is required for this action');
}
if (input.action === 'merge' && (!input.data.primaryCustomerId || !input.data.secondaryCustomerId)) {
errors.push('both primaryCustomerId and secondaryCustomerId are required for merge action');
}
return { valid: errors.length === 0, errors, warnings: [] };
}
getRequiredPermissions(): string[] {
return ['customer_access'];
}
getMetadata(): ActionHandlerMetadata {
return {
async: false,
timeout: 30000,
retryable: true,
maxRetries: 2
};
}
}
销售管道管理动作处理器
// 销售管道管理动作处理器
class SalesPipelineActionHandler implements IActionHandler {
name = 'sales-pipeline';
displayName = '销售管道管理';
category = ActionCategory.BUSINESS;
inputSchema: ActionInputSchema = {
type: 'object',
properties: {
action: {
type: 'string',
enum: ['create_opportunity', 'update_stage', 'forecast', 'win', 'lose'],
required: true
},
opportunityId: { type: 'string' },
stageId: { type: 'string' },
data: { type: 'object' },
options: { type: 'object' }
}
};
async execute(context: ActionExecutionContext): Promise<ActionResult> {
const { input, entityManager, user } = context;
try {
switch (input.action) {
case 'create_opportunity':
return await this.createOpportunity(input, entityManager, user);
case 'update_stage':
return await this.updateStage(input, entityManager, user);
case 'forecast':
return await this.generateForecast(input, entityManager, user);
case 'win':
return await this.winOpportunity(input, entityManager, user);
case 'lose':
return await this.loseOpportunity(input, entityManager, user);
default:
throw new Error(`Unknown pipeline action: ${input.action}`);
}
} catch (error) {
return {
success: false,
error: { message: error.message, code: 'PIPELINE_ACTION_ERROR' }
};
}
}
private async createOpportunity(input: any, entityManager: IEntityManager, user: any): Promise<ActionResult> {
// 验证客户存在
const customer = await entityManager.get('Customer', input.data.customerId);
if (!customer) {
throw new Error('Customer not found');
}
// 获取初始阶段
const initialStage = await entityManager.query({
entityType: 'OpportunityStage',
filter: { isActive: true },
sort: [{ field: 'sortOrder', direction: 'asc' }],
limit: 1
});
if (initialStage.data.length === 0) {
throw new Error('No active opportunity stages found');
}
// 创建销售机会
const opportunity = await entityManager.create('Opportunity', {
title: input.data.title,
description: input.data.description,
customerId: input.data.customerId,
amount: input.data.amount,
currency: input.data.currency || 'USD',
probability: initialStage.data[0].probability || 10,
expectedCloseDate: input.data.expectedCloseDate,
stageId: initialStage.data[0].id,
ownerId: input.data.ownerId || user.id,
teamId: input.data.teamId,
customFields: input.data.customFields || {}
});
// 创建初始活动记录
await entityManager.create('Activity', {
type: 'NOTE',
subject: 'Opportunity created',
description: `New opportunity created: ${opportunity.title} - ${opportunity.amount} ${opportunity.currency}`,
customerId: opportunity.customerId,
opportunityId: opportunity.id,
startDate: new Date(),
status: 'COMPLETED',
ownerId: user.id
});
// 更新客户总价值
await this.updateCustomerValue(opportunity.customerId, entityManager);
return {
success: true,
data: { opportunity },
events: [{
type: 'opportunity.created',
data: {
opportunityId: opportunity.id,
customerId: opportunity.customerId,
amount: opportunity.amount,
ownerId: opportunity.ownerId,
createdBy: user.id
}
}]
};
}
private async updateStage(input: any, entityManager: IEntityManager, user: any): Promise<ActionResult> {
const opportunity = await entityManager.get('Opportunity', input.opportunityId, {
include: ['stage']
});
if (!opportunity) {
throw new Error('Opportunity not found');
}
const newStage = await entityManager.get('OpportunityStage', input.stageId);
if (!newStage) {
throw new Error('Stage not found');
}
const oldStage = opportunity.stage;
// 验证阶段变更是否允许
const canMove = await this.validateStageTransition(opportunity, oldStage, newStage);
if (!canMove.allowed) {
throw new Error(canMove.reason);
}
// 更新机会阶段和概率
const updatedOpportunity = await entityManager.update('Opportunity', input.opportunityId, {
stageId: input.stageId,
probability: newStage.probability,
updatedAt: new Date()
});
// 创建阶段变更记录
await entityManager.create('Activity', {
type: 'NOTE',
subject: 'Stage updated',
description: `Opportunity moved from "${oldStage.name}" to "${newStage.name}"`,
customerId: opportunity.customerId,
opportunityId: opportunity.id,
startDate: new Date(),
status: 'COMPLETED',
ownerId: user.id
});
// 检查是否需要触发自动化操作
await this.triggerStageAutomation(opportunity, newStage, entityManager);
return {
success: true,
data: { opportunity: updatedOpportunity },
events: [{
type: 'opportunity.stage_changed',
data: {
opportunityId: opportunity.id,
fromStage: oldStage.name,
toStage: newStage.name,
probability: newStage.probability,
changedBy: user.id
}
}]
};
}
private async winOpportunity(input: any, entityManager: IEntityManager, user: any): Promise<ActionResult> {
const opportunity = await entityManager.get('Opportunity', input.opportunityId);
if (!opportunity) {
throw new Error('Opportunity not found');
}
if (opportunity.status !== 'OPEN') {
throw new Error('Only open opportunities can be won');
}
// 获取赢单阶段
const wonStage = await entityManager.query({
entityType: 'OpportunityStage',
filter: { isWon: true, isActive: true },
limit: 1
});
if (wonStage.data.length === 0) {
throw new Error('No won stage found');
}
const actualCloseDate = new Date();
// 更新机会状态
const updatedOpportunity = await entityManager.update('Opportunity', input.opportunityId, {
status: 'WON',
stageId: wonStage.data[0].id,
probability: 100,
actualCloseDate,
updatedAt: actualCloseDate
});
// 创建赢单记录
await entityManager.create('Activity', {
type: 'NOTE',
subject: 'Opportunity won',
description: `Opportunity won! Amount: ${opportunity.amount} ${opportunity.currency}`,
customerId: opportunity.customerId,
opportunityId: opportunity.id,
startDate: actualCloseDate,
status: 'COMPLETED',
ownerId: user.id,
outcome: `Won: ${opportunity.amount} ${opportunity.currency}`
});
// 更新客户生命周期价值
await this.updateCustomerLifetimeValue(opportunity.customerId, opportunity.amount, entityManager);
// 触发赢单后续流程(如生成合同、发票等)
await this.triggerWinAutomation(opportunity, entityManager);
return {
success: true,
data: { opportunity: updatedOpportunity },
events: [{
type: 'opportunity.won',
data: {
opportunityId: opportunity.id,
customerId: opportunity.customerId,
amount: opportunity.amount,
currency: opportunity.currency,
closedBy: user.id,
closeDate: actualCloseDate
}
}]
};
}
private async generateForecast(input: any, entityManager: IEntityManager, user: any): Promise<ActionResult> {
const period = input.options?.period || 'quarter'; // month, quarter, year
const ownerId = input.options?.ownerId || user.id;
// 获取时间范围
const dateRange = this.getForecastDateRange(period);
// 查询相关机会
const opportunities = await entityManager.query({
entityType: 'Opportunity',
filter: {
status: 'OPEN',
ownerId,
expectedCloseDate: {
$gte: dateRange.start,
$lte: dateRange.end
}
},
include: ['stage', 'customer']
});
// 计算预测数据
const forecast = this.calculateForecast(opportunities.data);
return {
success: true,
data: {
period,
dateRange,
forecast,
opportunities: opportunities.data
}
};
}
private calculateForecast(opportunities: any[]): any {
const forecast = {
totalOpportunities: opportunities.length,
totalValue: 0,
weightedValue: 0,
bestCase: 0,
worstCase: 0,
mostLikely: 0,
byStage: {} as any,
confidence: 0
};
opportunities.forEach(opp => {
const amount = parseFloat(opp.amount) || 0;
const probability = opp.probability || 0;
forecast.totalValue += amount;
forecast.weightedValue += amount * (probability / 100);
if (probability >= 75) {
forecast.bestCase += amount;
} else if (probability >= 25) {
forecast.mostLikely += amount;
} else {
forecast.worstCase += amount;
}
// 按阶段统计
const stageName = opp.stage?.name || 'Unknown';
if (!forecast.byStage[stageName]) {
forecast.byStage[stageName] = {
count: 0,
totalValue: 0,
weightedValue: 0
};
}
forecast.byStage[stageName].count += 1;
forecast.byStage[stageName].totalValue += amount;
forecast.byStage[stageName].weightedValue += amount * (probability / 100);
});
// 计算置信度
forecast.confidence = opportunities.length > 0 ?
Math.min(95, Math.max(10, opportunities.length * 5)) : 0;
return forecast;
}
private async validateStageTransition(opportunity: any, fromStage: any, toStage: any): Promise<{ allowed: boolean; reason?: string }> {
// 检查阶段是否激活
if (!toStage.isActive) {
return { allowed: false, reason: 'Target stage is not active' };
}
// 检查是否跳过了必要的阶段
if (toStage.sortOrder > fromStage.sortOrder + 1) {
const skippedStages = await this.getSkippedStages(fromStage.sortOrder, toStage.sortOrder);
if (skippedStages.some(stage => stage.requirements && stage.requirements.length > 0)) {
return { allowed: false, reason: 'Cannot skip stages with requirements' };
}
}
// 检查当前阶段的完成要求
if (fromStage.requirements && fromStage.requirements.length > 0) {
const requirementsMet = await this.checkStageRequirements(opportunity, fromStage.requirements);
if (!requirementsMet) {
return { allowed: false, reason: 'Current stage requirements not met' };
}
}
return { allowed: true };
}
private getForecastDateRange(period: string): { start: Date; end: Date } {
const now = new Date();
const start = new Date(now);
const end = new Date(now);
switch (period) {
case 'month':
start.setDate(1);
end.setMonth(end.getMonth() + 1, 0);
break;
case 'quarter':
const quarterStart = Math.floor(now.getMonth() / 3) * 3;
start.setMonth(quarterStart, 1);
end.setMonth(quarterStart + 3, 0);
break;
case 'year':
start.setMonth(0, 1);
end.setFullYear(end.getFullYear() + 1, 0, 0);
break;
}
return { start, end };
}
private async updateCustomerValue(customerId: string, entityManager: IEntityManager): Promise<void> {
// 计算客户的所有开放机会总价值
const opportunities = await entityManager.query({
entityType: 'Opportunity',
filter: { customerId, status: 'OPEN' }
});
const totalValue = opportunities.data.reduce((sum, opp) => sum + parseFloat(opp.amount || 0), 0);
await entityManager.update('Customer', customerId, {
totalValue,
updatedAt: new Date()
});
}
private async updateCustomerLifetimeValue(customerId: string, wonAmount: number, entityManager: IEntityManager): Promise<void> {
const customer = await entityManager.get('Customer', customerId);
const newLifetimeValue = (parseFloat(customer.lifetimeValue) || 0) + wonAmount;
await entityManager.update('Customer', customerId, {
lifetimeValue: newLifetimeValue,
updatedAt: new Date()
});
}
private async triggerStageAutomation(opportunity: any, stage: any, entityManager: IEntityManager): Promise<void> {
// 实现阶段自动化逻辑
// 例如:发送通知、创建任务、更新字段等
}
private async triggerWinAutomation(opportunity: any, entityManager: IEntityManager): Promise<void> {
// 实现赢单自动化逻辑
// 例如:生成合同、创建发票、发送庆祝邮件等
}
private async getSkippedStages(fromOrder: number, toOrder: number): Promise<any[]> {
// 获取跳过的阶段信息
return [];
}
private async checkStageRequirements(opportunity: any, requirements: string[]): Promise<boolean> {
// 检查阶段完成要求
return true;
}
async validate(input: any): Promise<ValidationResult> {
const errors: string[] = [];
if (!input.action) {
errors.push('action is required');
}
if (['update_stage', 'win', 'lose'].includes(input.action) && !input.opportunityId) {
errors.push('opportunityId is required for this action');
}
if (input.action === 'update_stage' && !input.stageId) {
errors.push('stageId is required for update_stage action');
}
if (input.action === 'create_opportunity' && !input.data) {
errors.push('data is required for create_opportunity action');
}
return { valid: errors.length === 0, errors, warnings: [] };
}
getRequiredPermissions(): string[] {
return ['sales_access'];
}
getMetadata(): ActionHandlerMetadata {
return {
async: false,
timeout: 30000,
retryable: true,
maxRetries: 2
};
}
}
视图定义
CRM仪表盘视图
// CRM仪表盘视图
export const CRMDashboardView: IViewDefinition = {
id: 'crm-dashboard',
name: 'CRMDashboard',
displayName: 'CRM仪表盘',
type: 'dashboard',
config: {
layout: {
type: 'grid',
columns: 12,
rows: 'auto'
},
widgets: [
{
id: 'sales-metrics',
type: 'metrics-card',
title: '销售指标',
position: { x: 0, y: 0, w: 6, h: 2 },
config: {
metrics: [
{
name: 'total_revenue',
displayName: '总收入',
query: {
entityType: 'Opportunity',
filter: { status: 'WON' },
aggregation: { field: 'amount', operation: 'sum' }
},
format: 'currency'
},
{
name: 'open_opportunities',
displayName: '开放机会',
query: {
entityType: 'Opportunity',
filter: { status: 'OPEN' },
aggregation: { operation: 'count' }
}
},
{
name: 'conversion_rate',
displayName: '转化率',
query: 'custom:conversion_rate',
format: 'percentage'
}
]
}
},
{
id: 'sales-pipeline',
type: 'pipeline-chart',
title: '销售管道',
position: { x: 6, y: 0, w: 6, h: 4 },
config: {
query: {
entityType: 'Opportunity',
filter: { status: 'OPEN' },
include: ['stage'],
groupBy: 'stage.name'
},
chartType: 'funnel',
showValues: true
}
},
{
id: 'recent-activities',
type: 'activity-feed',
title: '最近活动',
position: { x: 0, y: 2, w: 6, h: 4 },
config: {
query: {
entityType: 'Activity',
sort: [{ field: 'createdAt', direction: 'desc' }],
limit: 10,
include: ['customer', 'owner']
},
showAvatars: true,
groupByDate: true
}
},
{
id: 'top-customers',
type: 'customer-list',
title: '重点客户',
position: { x: 0, y: 6, w: 12, h: 3 },
config: {
query: {
entityType: 'Customer',
filter: { status: 'CUSTOMER' },
sort: [{ field: 'lifetimeValue', direction: 'desc' }],
limit: 5,
include: ['assignedTo']
},
showValue: true,
showLastContact: true
}
}
],
refreshInterval: 300000 // 5分钟
}
};
// 客户列表视图
export const CustomerListView: IViewDefinition = {
id: 'customer-list',
name: 'CustomerList',
displayName: '客户列表',
type: 'table',
entityType: 'Customer',
config: {
columns: [
{
name: 'customerNumber',
displayName: '客户编号',
type: 'string',
sortable: true,
searchable: true,
width: 120
},
{
name: 'fullName',
displayName: '客户名称',
type: 'string',
sortable: true,
searchable: true,
width: 200,
renderer: 'customer-name-link'
},
{
name: 'type',
displayName: '类型',
type: 'enum',
sortable: true,
filterable: true,
width: 80,
renderer: 'customer-type-badge'
},
{
name: 'status',
displayName: '状态',
type: 'enum',
sortable: true,
filterable: true,
width: 100,
renderer: 'customer-status-badge'
},
{
name: 'email',
displayName: '邮箱',
type: 'string',
sortable: true,
searchable: true,
width: 180
},
{
name: 'assignedTo.name',
displayName: '负责人',
type: 'string',
sortable: true,
filterable: true,
width: 120
},
{
name: 'totalValue',
displayName: '总价值',
type: 'currency',
sortable: true,
width: 120,
format: { currency: 'USD' }
},
{
name: 'lastContactDate',
displayName: '最后联系',
type: 'datetime',
sortable: true,
width: 140,
format: { dateStyle: 'short' }
},
{
name: 'createdAt',
displayName: '创建时间',
type: 'datetime',
sortable: true,
width: 140,
format: { dateStyle: 'short' }
}
],
defaultSort: [{ field: 'createdAt', direction: 'desc' }],
pageSize: 25,
enableSearch: true,
enableFilters: true,
filters: [
{
name: 'status',
displayName: '状态',
type: 'select',
options: [
{ label: '线索', value: 'LEAD' },
{ label: '潜在客户', value: 'PROSPECT' },
{ label: '客户', value: 'CUSTOMER' },
{ label: '非活跃', value: 'INACTIVE' }
]
},
{
name: 'type',
displayName: '类型',
type: 'select',
options: [
{ label: '个人', value: 'INDIVIDUAL' },
{ label: '公司', value: 'COMPANY' }
]
},
{
name: 'assignedTo',
displayName: '负责人',
type: 'select',
dataSource: 'User',
labelField: 'name',
valueField: 'id'
}
],
actions: [
{
name: 'create',
displayName: '新增客户',
type: 'primary',
icon: 'plus',
handler: 'navigate-to-create'
},
{
name: 'import',
displayName: '导入客户',
type: 'secondary',
icon: 'upload',
handler: 'import-customers-modal'
}
],
rowActions: [
{
name: 'view',
displayName: '查看详情',
icon: 'eye',
handler: 'navigate-to-detail'
},
{
name: 'edit',
displayName: '编辑',
icon: 'edit',
handler: 'navigate-to-edit'
},
{
name: 'convert',
displayName: '转换',
icon: 'arrow-right',
handler: 'convert-lead',
condition: { status: 'LEAD' }
}
]
}
};
// 销售管道视图
export const SalesPipelineView: IViewDefinition = {
id: 'sales-pipeline',
name: 'SalesPipeline',
displayName: '销售管道',
type: 'kanban',
entityType: 'Opportunity',
config: {
groupBy: 'stage.name',
groupConfig: {
dataSource: 'OpportunityStage',
sortBy: 'sortOrder',
labelField: 'name',
colorField: 'color'
},
cardFields: [
{
name: 'title',
displayName: '标题',
type: 'string',
primary: true
},
{
name: 'customer.fullName',
displayName: '客户',
type: 'string'
},
{
name: 'amount',
displayName: '金额',
type: 'currency',
format: { currency: 'USD' }
},
{
name: 'probability',
displayName: '概率',
type: 'percentage'
},
{
name: 'expectedCloseDate',
displayName: '预计成交',
type: 'date'
},
{
name: 'owner.name',
displayName: '负责人',
type: 'string'
}
],
cardActions: [
{
name: 'edit',
displayName: '编辑',
icon: 'edit',
handler: 'edit-opportunity'
},
{
name: 'activity',
displayName: '添加活动',
icon: 'plus',
handler: 'add-activity'
}
],
enableDragDrop: true,
showSummary: true,
summaryFields: ['count', 'totalAmount', 'avgAmount']
}
};
下一步
完成CRM系统的基础架构后,可以继续学习:
Last updated on