- 模块一: 出海案例作品集、博客动态发布、人名发音语音条、RTL阿语多语言切换 - 模块二: AI顾问人设配置、行业术语库润色、RAG切片沙盒调试、流式会话与意向留资卡片 - 模块三: 全球访客雷达大屏、多维加权意向算分、外部CRM对接推送、团队公海池流转 - 模块四: 移动端NFC安全挂失、NFC动态路由规则配置、动态防伪验签、展会制卡Zip批量导出 - 模块五: 组织架构与名片分配、移动端个人中心设置、离职交接与历史线索迁移、安全审计日志 - 模块六: Stripe国际信用卡订阅Webhook、独立二级域名与SSL检测、坐席配额预警、多币种发票 - 模块七: 纸质名片OCR解析入库、国际商务日程RFC 5545 ICS日历、微官网离线SWR预加载、展会实时排行榜大屏 - 模块八: 全球出海SEO OpenGraph元标签注入与社交爬虫探测、30点端到端全链路业务流集成测试套件
251 行
8.0 KiB
TypeScript
251 行
8.0 KiB
TypeScript
import { Provide } from '@midwayjs/core';
|
||
import { BaseService, CoolCommException } from '@cool-midway/core';
|
||
import { InjectEntityModel } from '@midwayjs/typeorm';
|
||
import { Repository, FindOptionsWhere } from 'typeorm';
|
||
import { CardInfoEntity } from '../../card/entity/info';
|
||
import { TeamHandoverLogEntity } from '../entity/handover-log';
|
||
import { CrmLeadEntity } from '../../crm/entity/lead';
|
||
import { CrmLeadAuditEntity } from '../../crm/entity/audit';
|
||
import {
|
||
ExecuteHandoverDTO,
|
||
HandoverRequest,
|
||
} from '../../../comm/types/omni';
|
||
import { CardPortalService } from '../../card/service/portal';
|
||
|
||
/**
|
||
* 离职资产安全交接服务
|
||
* 涵盖:名片短链重定向、历史CRM线索批量迁移/公海流转、不可逆安全工单审计
|
||
*/
|
||
@Provide()
|
||
export class TeamHandoverService extends BaseService {
|
||
@InjectEntityModel(CardInfoEntity)
|
||
cardInfoEntity: Repository<CardInfoEntity>;
|
||
|
||
@InjectEntityModel(TeamHandoverLogEntity)
|
||
teamHandoverLogEntity: Repository<TeamHandoverLogEntity>;
|
||
|
||
@InjectEntityModel(CrmLeadEntity)
|
||
crmLeadEntity: Repository<CrmLeadEntity>;
|
||
|
||
@InjectEntityModel(CrmLeadAuditEntity)
|
||
crmLeadAuditEntity: Repository<CrmLeadAuditEntity>;
|
||
|
||
/**
|
||
* 执行离职资产全流程安全交接
|
||
*/
|
||
async executeHandover(
|
||
dto: ExecuteHandoverDTO
|
||
): Promise<TeamHandoverLogEntity> {
|
||
const fromUserId = Number(dto?.fromUserId);
|
||
const toUserId =
|
||
dto?.toUserId !== undefined && dto?.toUserId !== null && dto?.toUserId !== ''
|
||
? Number(dto.toUserId)
|
||
: null;
|
||
|
||
if (!fromUserId) {
|
||
throw new CoolCommException('离职员工标识不能为空');
|
||
}
|
||
|
||
if (toUserId !== null && fromUserId === toUserId) {
|
||
throw new CoolCommException('接替人不能与原负责人相同');
|
||
}
|
||
|
||
const leadAction = dto.leadAction || 'none';
|
||
if (leadAction === 'transfer' && !toUserId) {
|
||
throw new CoolCommException('转移线索时接替人不能为空');
|
||
}
|
||
|
||
// 1. 离职员工名片短链安全重定向
|
||
const redirectType = dto.redirectType || 'none';
|
||
const redirectTarget = dto.redirectTarget || null;
|
||
|
||
if (dto.cardId) {
|
||
const cardId = Number(dto.cardId);
|
||
const card = await this.cardInfoEntity.findOneBy({ id: cardId });
|
||
if (!card) {
|
||
throw new CoolCommException('名片不存在');
|
||
}
|
||
if (Number(card.userId) !== fromUserId) {
|
||
throw new CoolCommException('名片不属于原负责人');
|
||
}
|
||
card.redirectType = redirectType;
|
||
card.redirectTarget = redirectTarget;
|
||
if (toUserId && redirectType !== 'portal') {
|
||
card.userId = toUserId;
|
||
}
|
||
this.syncInternalConfigRedirect(card, redirectType, redirectTarget);
|
||
await this.cardInfoEntity.save(card);
|
||
CardPortalService.clearAllCache();
|
||
} else if (redirectType !== 'none') {
|
||
const userCards = await this.cardInfoEntity.find({
|
||
where: { userId: fromUserId },
|
||
});
|
||
for (const c of userCards) {
|
||
c.redirectType = redirectType;
|
||
c.redirectTarget = redirectTarget;
|
||
if (toUserId && redirectType !== 'portal') {
|
||
c.userId = toUserId;
|
||
}
|
||
this.syncInternalConfigRedirect(c, redirectType, redirectTarget);
|
||
await this.cardInfoEntity.save(c);
|
||
}
|
||
if (userCards.length > 0) {
|
||
CardPortalService.clearAllCache();
|
||
}
|
||
}
|
||
|
||
// 2. 历史 CRM 客户线索批量原子流转 (转移至接任人或划入公海池)
|
||
const leads = await this.crmLeadEntity.find({
|
||
where: {
|
||
userId: fromUserId,
|
||
isPool: false,
|
||
},
|
||
});
|
||
|
||
const leadIds = leads.map(l => l.id);
|
||
let migratedLeadCount = 0;
|
||
|
||
if (leads.length > 0) {
|
||
const now = new Date();
|
||
if (leadAction === 'transfer' && toUserId) {
|
||
for (const lead of leads) {
|
||
lead.userId = toUserId;
|
||
lead.isPool = false;
|
||
lead.lastFollowUpTime = now;
|
||
}
|
||
await this.crmLeadEntity.save(leads);
|
||
|
||
const audits = leads.map(lead => {
|
||
const audit = new CrmLeadAuditEntity();
|
||
audit.leadId = lead.id;
|
||
audit.action = 'assign';
|
||
audit.operatorId = dto.operatorId ? Number(dto.operatorId) : null;
|
||
audit.fromUserId = fromUserId;
|
||
audit.toUserId = toUserId;
|
||
audit.reason = dto.remark || '员工离职资产交接-转移至接任人';
|
||
return audit;
|
||
});
|
||
await this.crmLeadAuditEntity.save(audits);
|
||
migratedLeadCount = leads.length;
|
||
} else if (leadAction === 'pool') {
|
||
for (const lead of leads) {
|
||
lead.userId = null;
|
||
lead.isPool = true;
|
||
lead.lastFollowUpTime = now;
|
||
}
|
||
await this.crmLeadEntity.save(leads);
|
||
|
||
const audits = leads.map(lead => {
|
||
const audit = new CrmLeadAuditEntity();
|
||
audit.leadId = lead.id;
|
||
audit.action = 'recycle';
|
||
audit.operatorId = dto.operatorId ? Number(dto.operatorId) : null;
|
||
audit.fromUserId = fromUserId;
|
||
audit.toUserId = null;
|
||
audit.reason = dto.remark || '员工离职资产交接-划入公海池';
|
||
return audit;
|
||
});
|
||
await this.crmLeadAuditEntity.save(audits);
|
||
migratedLeadCount = leads.length;
|
||
}
|
||
}
|
||
|
||
// 3. 写入不可逆安全交接工单历史表
|
||
const handoverNo = `HO${Date.now()}${Math.floor(Math.random() * 9000 + 1000)}`;
|
||
const snapshot = JSON.stringify({
|
||
fromUserId,
|
||
toUserId,
|
||
cardId: dto.cardId ? Number(dto.cardId) : null,
|
||
leadIds,
|
||
leadAction,
|
||
redirectType,
|
||
redirectTarget,
|
||
migratedLeadCount,
|
||
handoverTime: new Date().toISOString(),
|
||
});
|
||
|
||
const log = new TeamHandoverLogEntity();
|
||
log.handoverNo = handoverNo;
|
||
log.fromUserId = fromUserId;
|
||
log.toUserId = toUserId;
|
||
log.cardId = dto.cardId ? Number(dto.cardId) : null;
|
||
log.redirectType = redirectType;
|
||
log.redirectTarget = redirectTarget;
|
||
log.leadAction = leadAction;
|
||
log.migratedLeadCount = migratedLeadCount;
|
||
log.operatorId = dto.operatorId ? Number(dto.operatorId) : null;
|
||
log.status = 'completed';
|
||
log.remark = dto.remark || null;
|
||
log.snapshot = snapshot;
|
||
|
||
return await this.teamHandoverLogEntity.save(log);
|
||
}
|
||
|
||
/**
|
||
* 调阅离职交接工单历史列表
|
||
*/
|
||
async getHandoverLogs(query?: {
|
||
fromUserId?: number;
|
||
toUserId?: number;
|
||
}): Promise<TeamHandoverLogEntity[]> {
|
||
const where: FindOptionsWhere<TeamHandoverLogEntity> = {};
|
||
if (query?.fromUserId) where.fromUserId = Number(query.fromUserId);
|
||
if (query?.toUserId) where.toUserId = Number(query.toUserId);
|
||
return await this.teamHandoverLogEntity.find({
|
||
where: Object.keys(where).length > 0 ? where : undefined,
|
||
order: { id: 'DESC' },
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 兼容原有轻量 transfer 接口
|
||
*/
|
||
async transfer(req: HandoverRequest): Promise<CardInfoEntity> {
|
||
const cardId = Number(req?.cardId);
|
||
const fromUserId = Number(req?.fromUserId);
|
||
const toUserId = Number(req?.toUserId);
|
||
|
||
if (!cardId || !fromUserId || !toUserId) {
|
||
throw new CoolCommException('交接参数不完整');
|
||
}
|
||
|
||
await this.executeHandover({
|
||
cardId,
|
||
fromUserId,
|
||
toUserId,
|
||
redirectType: 'card',
|
||
redirectTarget: String(toUserId),
|
||
leadAction: 'transfer',
|
||
remark: '快速名片资产交接',
|
||
});
|
||
|
||
const card = await this.cardInfoEntity.findOneBy({ id: cardId });
|
||
if (!card) {
|
||
throw new CoolCommException('名片不存在');
|
||
}
|
||
return card;
|
||
}
|
||
|
||
/**
|
||
* 同步更新名片内部配置
|
||
*/
|
||
private syncInternalConfigRedirect(
|
||
card: CardInfoEntity,
|
||
redirectType: string,
|
||
redirectTarget: string | null
|
||
): void {
|
||
let cfg: Record<string, unknown> = {};
|
||
if (card.internalConfig) {
|
||
try {
|
||
cfg = JSON.parse(card.internalConfig) as Record<string, unknown>;
|
||
} catch {
|
||
cfg = {};
|
||
}
|
||
}
|
||
cfg.redirectType = redirectType;
|
||
cfg.redirectTarget = redirectTarget;
|
||
cfg.handoverAt = new Date().toISOString();
|
||
card.internalConfig = JSON.stringify(cfg);
|
||
}
|
||
}
|