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; @InjectEntityModel(TeamHandoverLogEntity) teamHandoverLogEntity: Repository; @InjectEntityModel(CrmLeadEntity) crmLeadEntity: Repository; @InjectEntityModel(CrmLeadAuditEntity) crmLeadAuditEntity: Repository; /** * 执行离职资产全流程安全交接 */ async executeHandover( dto: ExecuteHandoverDTO ): Promise { 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 { const where: FindOptionsWhere = {}; 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 { 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 = {}; if (card.internalConfig) { try { cfg = JSON.parse(card.internalConfig) as Record; } catch { cfg = {}; } } cfg.redirectType = redirectType; cfg.redirectTarget = redirectTarget; cfg.handoverAt = new Date().toISOString(); card.internalConfig = JSON.stringify(cfg); } }