feat: 完成全系统30个功能点开发与E2E全链路验收 (8轮调度32个子代理闭环)
- 模块一: 出海案例作品集、博客动态发布、人名发音语音条、RTL阿语多语言切换 - 模块二: AI顾问人设配置、行业术语库润色、RAG切片沙盒调试、流式会话与意向留资卡片 - 模块三: 全球访客雷达大屏、多维加权意向算分、外部CRM对接推送、团队公海池流转 - 模块四: 移动端NFC安全挂失、NFC动态路由规则配置、动态防伪验签、展会制卡Zip批量导出 - 模块五: 组织架构与名片分配、移动端个人中心设置、离职交接与历史线索迁移、安全审计日志 - 模块六: Stripe国际信用卡订阅Webhook、独立二级域名与SSL检测、坐席配额预警、多币种发票 - 模块七: 纸质名片OCR解析入库、国际商务日程RFC 5545 ICS日历、微官网离线SWR预加载、展会实时排行榜大屏 - 模块八: 全球出海SEO OpenGraph元标签注入与社交爬虫探测、30点端到端全链路业务流集成测试套件
这个提交包含在:
@@ -0,0 +1,185 @@
|
||||
import * as crypto from 'crypto';
|
||||
import axios from 'axios';
|
||||
import { Provide } from '@midwayjs/core';
|
||||
import { BaseService, CoolCommException } from '@cool-midway/core';
|
||||
import { CrmLeadEntity } from '../entity/lead';
|
||||
|
||||
/**
|
||||
* HubSpot Contacts v3 API 格式
|
||||
*/
|
||||
export interface HubSpotContactPayload {
|
||||
properties: {
|
||||
email: string;
|
||||
firstname: string;
|
||||
lastname: string;
|
||||
phone: string;
|
||||
company: string;
|
||||
lifecyclestage: 'lead';
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Salesforce Lead sObject 格式
|
||||
*/
|
||||
export interface SalesforceLeadPayload {
|
||||
FirstName: string;
|
||||
LastName: string;
|
||||
Email: string;
|
||||
Company: string;
|
||||
Phone: string;
|
||||
Status: 'Open - Not Contacted';
|
||||
}
|
||||
|
||||
/**
|
||||
* Webhook 发送配置选项
|
||||
*/
|
||||
export interface WebhookSendOptions {
|
||||
maxRetries?: number;
|
||||
delayMs?: number;
|
||||
timeoutMs?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Webhook 发送响应结果
|
||||
*/
|
||||
export interface WebhookResult {
|
||||
success: boolean;
|
||||
attempts: number;
|
||||
data?: unknown;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 国际主流 CRM (HubSpot / Salesforce / Webhook) 对接服务
|
||||
*/
|
||||
@Provide()
|
||||
export class CrmSyncService extends BaseService {
|
||||
/**
|
||||
* 智能拆分姓名
|
||||
*/
|
||||
private splitName(name?: string): { firstName: string; lastName: string } {
|
||||
const parts = (name || '').trim().split(/\s+/).filter(Boolean);
|
||||
if (parts.length === 0) {
|
||||
return { firstName: '', lastName: '' };
|
||||
}
|
||||
if (parts.length === 1) {
|
||||
return { firstName: parts[0], lastName: '' };
|
||||
}
|
||||
return {
|
||||
firstName: parts[0],
|
||||
lastName: parts.slice(1).join(' '),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 异步延时工具
|
||||
*/
|
||||
private sleep(ms: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换成 HubSpot Contacts v3 API 格式
|
||||
*/
|
||||
formatToHubSpot(lead: Partial<CrmLeadEntity>): HubSpotContactPayload {
|
||||
const { firstName, lastName } = this.splitName(lead?.name);
|
||||
return {
|
||||
properties: {
|
||||
email: lead?.email || '',
|
||||
firstname: firstName,
|
||||
lastname: lastName,
|
||||
phone: lead?.phone || '',
|
||||
company: lead?.company || '',
|
||||
lifecyclestage: 'lead',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换成 Salesforce Lead sObject 格式
|
||||
*/
|
||||
formatToSalesforce(lead: Partial<CrmLeadEntity>): SalesforceLeadPayload {
|
||||
const { firstName, lastName } = this.splitName(lead?.name);
|
||||
let finalFirstName = firstName;
|
||||
let finalLastName = lastName;
|
||||
|
||||
// Salesforce 规范:LastName 必填;若仅单名,LastName 取该名,FirstName 为空;若全无,LastName 为 'Unknown'
|
||||
if (!lastName) {
|
||||
finalFirstName = '';
|
||||
finalLastName = firstName || 'Unknown';
|
||||
}
|
||||
|
||||
return {
|
||||
FirstName: finalFirstName,
|
||||
LastName: finalLastName,
|
||||
Email: lead?.email || '',
|
||||
Company: lead?.company || 'Unknown',
|
||||
Phone: lead?.phone || '',
|
||||
Status: 'Open - Not Contacted',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算 HMAC-SHA256 签名
|
||||
*/
|
||||
generateSignature(payload: unknown, secret: string): string {
|
||||
const content =
|
||||
typeof payload === 'string' ? payload : JSON.stringify(payload);
|
||||
return crypto.createHmac('sha256', secret).update(content).digest('hex');
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送带 HMAC-SHA256 签名的 Webhook,具备模拟退避重试(最多 3 次)
|
||||
*/
|
||||
async sendWebhook(
|
||||
url: string,
|
||||
secret: string,
|
||||
lead: Partial<CrmLeadEntity>,
|
||||
options?: WebhookSendOptions
|
||||
): Promise<WebhookResult> {
|
||||
if (!url || !url.trim()) {
|
||||
throw new CoolCommException('缺少目标 Webhook URL', 400);
|
||||
}
|
||||
|
||||
const payload = {
|
||||
event: 'crm.lead.sync',
|
||||
timestamp: Date.now(),
|
||||
data: lead,
|
||||
};
|
||||
|
||||
const signature = this.generateSignature(payload, secret || '');
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'X-OmniCard-Signature': signature,
|
||||
};
|
||||
|
||||
const maxRetries = options?.maxRetries ?? 3;
|
||||
const baseDelay = options?.delayMs ?? 100;
|
||||
const timeout = options?.timeoutMs ?? 5000;
|
||||
|
||||
let lastError: Error | null = null;
|
||||
|
||||
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
||||
try {
|
||||
const response = await axios.post(url, payload, { headers, timeout });
|
||||
return {
|
||||
success: true,
|
||||
attempts: attempt,
|
||||
data: response.data,
|
||||
};
|
||||
} catch (err: unknown) {
|
||||
lastError = err instanceof Error ? err : new Error(String(err));
|
||||
if (attempt < maxRetries) {
|
||||
const delay = baseDelay * Math.pow(2, attempt - 1);
|
||||
await this.sleep(delay);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
attempts: maxRetries,
|
||||
error: lastError?.message || 'Webhook request failed',
|
||||
};
|
||||
}
|
||||
}
|
||||
在新工单中引用
屏蔽一个用户