更新
This commit is contained in:
@@ -340,8 +340,8 @@ class ConversionLogic
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the distinct external contacts behind an add_fans_count row.
|
||||
/**
|
||||
* Return the add events behind an add_fans_count row.
|
||||
*
|
||||
* The caller must pass the already-authorized admin range and the exact
|
||||
* department ids represented by the clicked tree node. This keeps the
|
||||
@@ -505,7 +505,7 @@ class ConversionLogic
|
||||
return $empty;
|
||||
}
|
||||
|
||||
$pairs = self::loadFanDetailRows(
|
||||
$pairs = self::loadFanDetailRows(
|
||||
$startTimestamp,
|
||||
$endTimestamp,
|
||||
$mediaChannel,
|
||||
@@ -515,15 +515,20 @@ class ConversionLogic
|
||||
);
|
||||
// Target membership has already been resolved before the event query,
|
||||
// so sorting and pagination operate on the clicked row's small set.
|
||||
$matched = $pairs;
|
||||
$matched = $pairs;
|
||||
|
||||
usort($matched, static function (array $left, array $right): int {
|
||||
$timeCompare = ((int) ($right['add_time'] ?? 0)) <=> ((int) ($left['add_time'] ?? 0));
|
||||
if ($timeCompare !== 0) {
|
||||
return $timeCompare;
|
||||
}
|
||||
|
||||
return strcmp(
|
||||
if ($timeCompare !== 0) {
|
||||
return $timeCompare;
|
||||
}
|
||||
|
||||
$eventCompare = ((int) ($right['add_event_id'] ?? 0)) <=> ((int) ($left['add_event_id'] ?? 0));
|
||||
if ($eventCompare !== 0) {
|
||||
return $eventCompare;
|
||||
}
|
||||
|
||||
return strcmp(
|
||||
(string) ($left['external_userid'] ?? ''),
|
||||
(string) ($right['external_userid'] ?? '')
|
||||
) ?: strcmp(
|
||||
@@ -552,45 +557,45 @@ class ConversionLogic
|
||||
$pageRows,
|
||||
static fn (array $row): bool => !empty($row['is_deleted'])
|
||||
));
|
||||
$deleteTimesByPair = [];
|
||||
if ($deletedPageRows !== []) {
|
||||
$deletedUserIds = array_values(array_unique(array_filter(array_map(
|
||||
static fn (array $row): string => trim((string) ($row['user_id'] ?? '')),
|
||||
$deletedPageRows
|
||||
))));
|
||||
$deletedExternalUserIds = array_values(array_unique(array_filter(array_map(
|
||||
static fn (array $row): string => trim((string) ($row['external_userid'] ?? '')),
|
||||
$deletedPageRows
|
||||
))));
|
||||
$deletedEvents = Db::name('qywx_external_contact_event')
|
||||
->where('change_type', 'del_external_contact')
|
||||
->where('event_time', '<=', $endTimestamp)
|
||||
->whereIn('user_id', $deletedUserIds)
|
||||
->whereIn('external_userid', $deletedExternalUserIds)
|
||||
->field('user_id,external_userid,MAX(event_time) AS delete_time')
|
||||
->group('user_id,external_userid')
|
||||
->select()
|
||||
->toArray();
|
||||
foreach ($deletedEvents as $deletedEvent) {
|
||||
$key = trim((string) ($deletedEvent['user_id'] ?? ''))
|
||||
. "\0"
|
||||
. trim((string) ($deletedEvent['external_userid'] ?? ''));
|
||||
$deleteTimesByPair[$key] = max(0, (int) ($deletedEvent['delete_time'] ?? 0));
|
||||
}
|
||||
}
|
||||
$deleteTimesByPair = [];
|
||||
if ($deletedPageRows !== []) {
|
||||
$deletedUserIds = array_values(array_unique(array_filter(array_map(
|
||||
static fn (array $row): string => trim((string) ($row['user_id'] ?? '')),
|
||||
$deletedPageRows
|
||||
))));
|
||||
$deletedExternalUserIds = array_values(array_unique(array_filter(array_map(
|
||||
static fn (array $row): string => trim((string) ($row['external_userid'] ?? '')),
|
||||
$deletedPageRows
|
||||
))));
|
||||
$deletedEvents = Db::name('qywx_external_contact_event')
|
||||
->where('change_type', 'del_external_contact')
|
||||
->where('event_time', '<=', $endTimestamp)
|
||||
->whereIn('user_id', $deletedUserIds)
|
||||
->whereIn('external_userid', $deletedExternalUserIds)
|
||||
->field('user_id,external_userid,MAX(event_time) AS delete_time')
|
||||
->group('user_id,external_userid')
|
||||
->select()
|
||||
->toArray();
|
||||
foreach ($deletedEvents as $deletedEvent) {
|
||||
$key = trim((string) ($deletedEvent['user_id'] ?? ''))
|
||||
. "\0"
|
||||
. trim((string) ($deletedEvent['external_userid'] ?? ''));
|
||||
$deleteTimesByPair[$key] = max(0, (int) ($deletedEvent['delete_time'] ?? 0));
|
||||
}
|
||||
}
|
||||
$staffNames = self::resolveQywxUserNames(array_values(array_unique(array_filter(array_map(
|
||||
static fn (array $row): string => trim((string) ($row['user_id'] ?? '')),
|
||||
$pageRows
|
||||
)))));
|
||||
|
||||
$lists = array_map(static function (array $row) use ($customerNames, $staffNames, $deleteTimesByPair): array {
|
||||
$externalUserId = trim((string) ($row['external_userid'] ?? ''));
|
||||
$wecomUserId = trim((string) ($row['user_id'] ?? ''));
|
||||
$addTime = max(0, (int) ($row['add_time'] ?? 0));
|
||||
$deleted = !empty($row['is_deleted']);
|
||||
$deleteTime = $deleted
|
||||
? max(0, (int) ($deleteTimesByPair[$wecomUserId . "\0" . $externalUserId] ?? 0))
|
||||
: 0;
|
||||
$lists = array_map(static function (array $row) use ($customerNames, $staffNames, $deleteTimesByPair): array {
|
||||
$externalUserId = trim((string) ($row['external_userid'] ?? ''));
|
||||
$wecomUserId = trim((string) ($row['user_id'] ?? ''));
|
||||
$addTime = max(0, (int) ($row['add_time'] ?? 0));
|
||||
$deleted = !empty($row['is_deleted']);
|
||||
$deleteTime = $deleted
|
||||
? max(0, (int) ($deleteTimesByPair[$wecomUserId . "\0" . $externalUserId] ?? 0))
|
||||
: 0;
|
||||
if ($deleteTime < $addTime) {
|
||||
$deleteTime = 0;
|
||||
}
|
||||
@@ -606,18 +611,18 @@ class ConversionLogic
|
||||
];
|
||||
}, $pageRows);
|
||||
|
||||
return [
|
||||
'lists' => $lists,
|
||||
'count' => $count,
|
||||
'deleted_count' => $deletedCount,
|
||||
return [
|
||||
'lists' => $lists,
|
||||
'count' => $count,
|
||||
'deleted_count' => $deletedCount,
|
||||
'page_no' => $pageNo,
|
||||
'page_size' => $pageSize,
|
||||
'date_range' => [$startDate, $endDate],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Department scope imposed by an actively maintained media-channel cost
|
||||
'date_range' => [$startDate, $endDate],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Department scope imposed by an actively maintained media-channel cost
|
||||
* binding. null means the channel has no binding and therefore does not
|
||||
* restrict business statistics.
|
||||
*
|
||||
@@ -1262,17 +1267,17 @@ class ConversionLogic
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 区间新增加粉:按企微员工聚合后,再投影到部门/成员/虚拟桶。
|
||||
*
|
||||
* 口径(对齐企微客户列表 / 官方「新增客户」不含继承,而非原始回调条数):
|
||||
* - 统计区间内 add_external_contact,按 (user_id, external_userid) 去重;
|
||||
* - 同一员工在区间开始前已加过该客户的重加不计(企微「添加时间」仍是首次跟进时间,
|
||||
* 删后再加会再推 add_external_contact,但不能当当天新客,否则会跨日重复计);
|
||||
/**
|
||||
* 区间新增客户关系:按企微员工聚合后,再投影到部门/成员/虚拟桶。
|
||||
*
|
||||
* 口径:
|
||||
* - 以 (user_id, external_userid) 作为唯一组合,同一员工的同一客户只计一次;
|
||||
* - 同一客户添加到不同员工名下时,因 user_id 不同,每名员工分别计一次;
|
||||
* - 同一组合在区间开始前已经产生过 add_external_contact 时,不再算区间新增;
|
||||
* - add_external_contact 是企微确认客户关系已建立后的权威事件;会话存档同意
|
||||
* msg_audit_approved 属于独立能力,不能作为加粉前置条件,否则未开通会话存档的员工会被整批清零;
|
||||
* - 加粉之后、统计结束前若出现 del_external_contact(客户从企微侧删除),仍计入加粉总数,
|
||||
* 并额外计入已删提示子集;不处理 del_follow_user:企微客户列表中改名带「删」的客户往往仍在列表中;
|
||||
* - 组合在统计结束时已删除,仍计入加粉总数,并额外计入已删提示子集;
|
||||
* 不处理 del_follow_user:企微客户列表中改名带「删」的客户往往仍在列表中;
|
||||
* - 剔除非投放加粉:跟进人 add_way∈{1 扫一扫, 2 搜索手机号, 3 名片分享};
|
||||
* - 剔除继承客户:跟进人 add_way∈{201 内部成员共享, 202 管理员/负责人分配}(含在职/离职继承)。
|
||||
*
|
||||
@@ -1285,25 +1290,21 @@ class ConversionLogic
|
||||
int $endTimestamp,
|
||||
?array $mediaChannel,
|
||||
?array $adminIds = null
|
||||
): array {
|
||||
$detailRows = self::loadFanDetailRows($startTimestamp, $endTimestamp, $mediaChannel, $adminIds);
|
||||
$effectivePairs = array_values(array_filter(
|
||||
$detailRows,
|
||||
static fn (array $row): bool => empty($row['is_deleted'])
|
||||
));
|
||||
|
||||
return self::buildFanCountRows($detailRows, $effectivePairs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the exact distinct (user_id, external_userid) pairs represented by
|
||||
* add_fans_count. Both aggregate statistics and the detail endpoint consume
|
||||
* this method, so channel, add-way, prior-add and deletion rules cannot
|
||||
* drift apart.
|
||||
): array {
|
||||
$detailRows = self::loadFanDetailRows($startTimestamp, $endTimestamp, $mediaChannel, $adminIds);
|
||||
|
||||
return self::buildFanCountRows($detailRows);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the exact distinct (user_id, external_userid) pairs represented by
|
||||
* add_fans_count. Both aggregate statistics and the detail endpoint consume
|
||||
* this method, so channel, add-way, prior-add and deletion rules cannot
|
||||
* drift apart.
|
||||
*
|
||||
* @param array<string, mixed>|null $mediaChannel
|
||||
* @param int[]|null $adminIds
|
||||
* @return array<int, array{user_id:string,external_userid:string,add_time:int,is_deleted:bool,delete_time:int}>
|
||||
* @return array<int, array{add_event_id:int,user_id:string,external_userid:string,add_time:int,is_deleted:bool,delete_time:int}>
|
||||
*/
|
||||
private static function loadFanDetailRows(
|
||||
int $startTimestamp,
|
||||
@@ -1335,7 +1336,7 @@ class ConversionLogic
|
||||
}
|
||||
}
|
||||
|
||||
$baseKey = self::requestRowsCacheKey('fans-detail-v1', [
|
||||
$baseKey = self::requestRowsCacheKey('fans-detail-v3-employee-customer', [
|
||||
$startTimestamp,
|
||||
$endTimestamp,
|
||||
self::mediaChannelCacheKey($mediaChannel),
|
||||
@@ -1379,23 +1380,56 @@ class ConversionLogic
|
||||
}
|
||||
}
|
||||
|
||||
$eventTable = config('database.connections.mysql.prefix') . 'qywx_external_contact_event';
|
||||
$query = Db::name('qywx_external_contact_event')
|
||||
->alias('e')
|
||||
->where('e.change_type', 'add_external_contact')
|
||||
->where('e.event_time', 'between', [$startTimestamp, $endTimestamp])
|
||||
->where('e.user_id', '<>', '')
|
||||
->where('e.external_userid', '<>', '')
|
||||
->whereRaw(
|
||||
'NOT EXISTS (SELECT 1 FROM `' . $eventTable . '` prev_e'
|
||||
. ' WHERE prev_e.user_id = e.user_id'
|
||||
. ' AND prev_e.external_userid = e.external_userid'
|
||||
. ' AND prev_e.change_type = ?'
|
||||
. ' AND prev_e.event_time < ?)',
|
||||
['add_external_contact', $startTimestamp]
|
||||
)
|
||||
->field('e.user_id,e.external_userid,MIN(e.event_time) AS add_time')
|
||||
->group('e.user_id, e.external_userid');
|
||||
$eventTable = config('database.connections.mysql.prefix') . 'qywx_external_contact_event';
|
||||
$livePairPredicate =
|
||||
'EXISTS (SELECT 1 FROM `' . $eventTable . '` surviving_e'
|
||||
. ' WHERE surviving_e.user_id = e.user_id'
|
||||
. ' AND surviving_e.external_userid = e.external_userid'
|
||||
. ' AND surviving_e.change_type = ?'
|
||||
. ' AND surviving_e.event_time >= ?'
|
||||
. ' AND surviving_e.event_time <= ?'
|
||||
. ' AND NOT EXISTS (SELECT 1 FROM `' . $eventTable . '` surviving_del'
|
||||
. ' WHERE surviving_del.user_id = surviving_e.user_id'
|
||||
. ' AND surviving_del.external_userid = surviving_e.external_userid'
|
||||
. ' AND surviving_del.change_type = ?'
|
||||
. ' AND surviving_del.event_time <= ?'
|
||||
. ' AND (surviving_del.event_time > surviving_e.event_time'
|
||||
. ' OR (surviving_del.event_time = surviving_e.event_time'
|
||||
. ' AND surviving_del.id > surviving_e.id))))';
|
||||
$livePairBindings = [
|
||||
'add_external_contact',
|
||||
$startTimestamp,
|
||||
$endTimestamp,
|
||||
'del_external_contact',
|
||||
$endTimestamp,
|
||||
];
|
||||
$query = Db::name('qywx_external_contact_event')
|
||||
->alias('e')
|
||||
->where('e.change_type', 'add_external_contact')
|
||||
->where('e.event_time', 'between', [$startTimestamp, $endTimestamp])
|
||||
->where('e.user_id', '<>', '')
|
||||
->where('e.external_userid', '<>', '')
|
||||
->whereRaw(
|
||||
'NOT EXISTS (SELECT 1 FROM `' . $eventTable . '` prev_e'
|
||||
. ' WHERE prev_e.user_id = e.user_id'
|
||||
. ' AND prev_e.external_userid = e.external_userid'
|
||||
. ' AND prev_e.change_type = ?'
|
||||
. ' AND prev_e.event_time < ?)',
|
||||
['add_external_contact', $startTimestamp]
|
||||
)
|
||||
// 同一员工+客户只保留区间内最早事件;不同员工拥有不同 user_id,会分别保留。
|
||||
->whereRaw(
|
||||
'NOT EXISTS (SELECT 1 FROM `' . $eventTable . '` earlier_e'
|
||||
. ' WHERE earlier_e.user_id = e.user_id'
|
||||
. ' AND earlier_e.external_userid = e.external_userid'
|
||||
. ' AND earlier_e.change_type = ?'
|
||||
. ' AND earlier_e.event_time >= ?'
|
||||
. ' AND earlier_e.event_time <= ?'
|
||||
. ' AND (earlier_e.event_time < e.event_time'
|
||||
. ' OR (earlier_e.event_time = e.event_time AND earlier_e.id < e.id)))',
|
||||
['add_external_contact', $startTimestamp, $endTimestamp]
|
||||
)
|
||||
->field('e.id AS add_event_id,e.user_id,e.external_userid,e.event_time AS add_time');
|
||||
if ($workWechatUserIds !== null) {
|
||||
$query->whereIn('e.user_id', $workWechatUserIds);
|
||||
} elseif ($unboundOnly) {
|
||||
@@ -1406,58 +1440,49 @@ class ConversionLogic
|
||||
. ' AND bound_a.delete_time IS NULL)'
|
||||
);
|
||||
}
|
||||
$effectiveQuery = clone $query;
|
||||
if ($mediaChannel !== null) {
|
||||
// 有效加粉保持原口径:只按未删客户的当前标签/渠道关系筛选。
|
||||
MediaChannelService::applyExternalUserChannelFilter($effectiveQuery, 'e.external_userid', $mediaChannel);
|
||||
}
|
||||
$effectivePairs = $effectiveQuery
|
||||
->whereRaw(
|
||||
'EXISTS (SELECT 1 FROM `' . $eventTable . '` surviving_e'
|
||||
. ' WHERE surviving_e.user_id = e.user_id'
|
||||
. ' AND surviving_e.external_userid = e.external_userid'
|
||||
. ' AND surviving_e.change_type = ?'
|
||||
. ' AND surviving_e.event_time >= ?'
|
||||
. ' AND surviving_e.event_time <= ?'
|
||||
. ' AND NOT EXISTS (SELECT 1 FROM `' . $eventTable . '` surviving_del'
|
||||
. ' WHERE surviving_del.user_id = surviving_e.user_id'
|
||||
. ' AND surviving_del.external_userid = surviving_e.external_userid'
|
||||
. ' AND surviving_del.change_type = ?'
|
||||
. ' AND surviving_del.event_time >= surviving_e.event_time'
|
||||
. ' AND surviving_del.event_time <= ?))',
|
||||
['add_external_contact', $startTimestamp, $endTimestamp, 'del_external_contact', $endTimestamp]
|
||||
)
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
$deletedQuery = clone $query;
|
||||
if ($mediaChannel !== null) {
|
||||
// del_external_contact 会软删 contact 并清掉关系表,已删归属改用保留的 follow_users 快照。
|
||||
MediaChannelService::applyHistoricalExternalUserChannelFilter($deletedQuery, 'e.external_userid', $mediaChannel);
|
||||
}
|
||||
$deletedPairs = $deletedQuery
|
||||
->whereRaw(
|
||||
'NOT EXISTS (SELECT 1 FROM `' . $eventTable . '` surviving_e'
|
||||
. ' WHERE surviving_e.user_id = e.user_id'
|
||||
. ' AND surviving_e.external_userid = e.external_userid'
|
||||
. ' AND surviving_e.change_type = ?'
|
||||
. ' AND surviving_e.event_time >= ?'
|
||||
. ' AND surviving_e.event_time <= ?'
|
||||
. ' AND NOT EXISTS (SELECT 1 FROM `' . $eventTable . '` surviving_del'
|
||||
. ' WHERE surviving_del.user_id = surviving_e.user_id'
|
||||
. ' AND surviving_del.external_userid = surviving_e.external_userid'
|
||||
. ' AND surviving_del.change_type = ?'
|
||||
. ' AND surviving_del.event_time >= surviving_e.event_time'
|
||||
. ' AND surviving_del.event_time <= ?))',
|
||||
['add_external_contact', $startTimestamp, $endTimestamp, 'del_external_contact', $endTimestamp]
|
||||
)
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
// add_way 筛选对有效与已删粉丝使用同一口径。
|
||||
$candidatePairs = array_merge($effectivePairs, $deletedPairs);
|
||||
$candidatePairs = self::excludeUncountedFanPairs($candidatePairs);
|
||||
$rows = self::buildFanDetailRows($candidatePairs, $effectivePairs);
|
||||
$effectiveQuery = clone $query;
|
||||
if ($mediaChannel !== null) {
|
||||
MediaChannelService::applyExternalUserEventChannelFilter(
|
||||
$effectiveQuery,
|
||||
'e.id',
|
||||
'e.external_userid',
|
||||
'e.user_id',
|
||||
$mediaChannel
|
||||
);
|
||||
}
|
||||
$effectivePairs = $effectiveQuery
|
||||
->whereRaw($livePairPredicate, $livePairBindings)
|
||||
->select()
|
||||
->toArray();
|
||||
foreach ($effectivePairs as &$effectivePair) {
|
||||
$effectivePair['is_deleted'] = false;
|
||||
}
|
||||
unset($effectivePair);
|
||||
|
||||
$deletedQuery = clone $query;
|
||||
if ($mediaChannel !== null) {
|
||||
MediaChannelService::applyExternalUserEventChannelFilter(
|
||||
$deletedQuery,
|
||||
'e.id',
|
||||
'e.external_userid',
|
||||
'e.user_id',
|
||||
$mediaChannel,
|
||||
true
|
||||
);
|
||||
}
|
||||
$deletedPairs = $deletedQuery
|
||||
->whereRaw('NOT (' . $livePairPredicate . ')', $livePairBindings)
|
||||
->select()
|
||||
->toArray();
|
||||
foreach ($deletedPairs as &$deletedPair) {
|
||||
$deletedPair['is_deleted'] = true;
|
||||
}
|
||||
unset($deletedPair);
|
||||
|
||||
// add_way 筛选对有效与已删组合使用同一口径。
|
||||
$candidatePairs = array_merge($effectivePairs, $deletedPairs);
|
||||
$candidatePairs = self::excludeUncountedFanPairs($candidatePairs);
|
||||
$rows = self::buildFanDetailRows($candidatePairs);
|
||||
|
||||
self::$requestRowsCache[$cacheKey] = $rows;
|
||||
|
||||
@@ -1465,97 +1490,69 @@ class ConversionLogic
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int,array<string,mixed>> $candidatePairs
|
||||
* @param array<int,array<string,mixed>> $effectivePairs
|
||||
* @return array<int, array{user_id:string,external_userid:string,add_time:int,is_deleted:bool,delete_time:int}>
|
||||
*/
|
||||
private static function buildFanDetailRows(
|
||||
array $candidatePairs,
|
||||
array $effectivePairs
|
||||
): array {
|
||||
$effectiveKeys = [];
|
||||
foreach ($effectivePairs as $pair) {
|
||||
$userId = trim((string) ($pair['user_id'] ?? ''));
|
||||
$externalUserId = trim((string) ($pair['external_userid'] ?? ''));
|
||||
if ($userId !== '' && $externalUserId !== '') {
|
||||
$effectiveKeys[$userId . "\0" . $externalUserId] = true;
|
||||
}
|
||||
}
|
||||
|
||||
$detailsByKey = [];
|
||||
foreach ($candidatePairs as $pair) {
|
||||
$userId = trim((string) ($pair['user_id'] ?? ''));
|
||||
$externalUserId = trim((string) ($pair['external_userid'] ?? ''));
|
||||
if ($userId === '' || $externalUserId === '') {
|
||||
continue;
|
||||
}
|
||||
$key = $userId . "\0" . $externalUserId;
|
||||
$addTime = max(0, (int) ($pair['add_time'] ?? 0));
|
||||
if (isset($detailsByKey[$key])) {
|
||||
$existingTime = (int) ($detailsByKey[$key]['add_time'] ?? 0);
|
||||
if ($addTime > 0 && ($existingTime <= 0 || $addTime < $existingTime)) {
|
||||
$detailsByKey[$key]['add_time'] = $addTime;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
$detailsByKey[$key] = [
|
||||
'user_id' => $userId,
|
||||
'external_userid' => $externalUserId,
|
||||
'add_time' => $addTime,
|
||||
'is_deleted' => !isset($effectiveKeys[$key]),
|
||||
'delete_time' => 0,
|
||||
];
|
||||
}
|
||||
|
||||
return array_values($detailsByKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按员工聚合全部候选新增,并标记其中期末已删除的子集。
|
||||
*
|
||||
* 候选对已经过“区间前未添加”、渠道与 add_way 规则,每个去重候选对都计入
|
||||
* add_fans_count;有效对是期末未删除的子集,因此候选对与有效对的差集另计入
|
||||
* deleted_fans_count。deleted_fans_count 只是 add_fans_count 的提示子集,不再扣减或重复累计。
|
||||
* 若区间内删除后又重加且期末仍有效,该对仍在有效子集中,不会误计为已删。
|
||||
*
|
||||
* @param array<int, array<string, mixed>> $candidatePairs
|
||||
* @param array<int, array<string, mixed>> $effectivePairs
|
||||
* @return array<int, array{user_id: string, add_fans_count: int, deleted_fans_count: int}>
|
||||
*/
|
||||
private static function buildFanCountRows(array $candidatePairs, array $effectivePairs): array
|
||||
{
|
||||
$effectiveKeys = [];
|
||||
foreach ($effectivePairs as $pair) {
|
||||
$userId = trim((string) ($pair['user_id'] ?? ''));
|
||||
$externalUserId = trim((string) ($pair['external_userid'] ?? ''));
|
||||
if ($userId === '' || $externalUserId === '') {
|
||||
continue;
|
||||
}
|
||||
$effectiveKeys[$userId . "\0" . $externalUserId] = true;
|
||||
}
|
||||
|
||||
$countsByUser = [];
|
||||
$seenCandidateKeys = [];
|
||||
foreach ($candidatePairs as $pair) {
|
||||
$userId = trim((string) ($pair['user_id'] ?? ''));
|
||||
$externalUserId = trim((string) ($pair['external_userid'] ?? ''));
|
||||
if ($userId === '' || $externalUserId === '') {
|
||||
continue;
|
||||
}
|
||||
$key = $userId . "\0" . $externalUserId;
|
||||
if (isset($seenCandidateKeys[$key])) {
|
||||
continue;
|
||||
}
|
||||
$seenCandidateKeys[$key] = true;
|
||||
$countsByUser[$userId] ??= [
|
||||
* @param array<int,array<string,mixed>> $candidatePairs
|
||||
* @return array<int, array{add_event_id:int,user_id:string,external_userid:string,add_time:int,is_deleted:bool,delete_time:int}>
|
||||
*/
|
||||
private static function buildFanDetailRows(array $candidatePairs): array
|
||||
{
|
||||
$detailsByPair = [];
|
||||
foreach ($candidatePairs as $pair) {
|
||||
$eventId = max(0, (int) ($pair['add_event_id'] ?? 0));
|
||||
$userId = trim((string) ($pair['user_id'] ?? ''));
|
||||
$externalUserId = trim((string) ($pair['external_userid'] ?? ''));
|
||||
if ($eventId <= 0 || $userId === '' || $externalUserId === '') {
|
||||
continue;
|
||||
}
|
||||
$key = $userId . "\0" . $externalUserId;
|
||||
if (isset($detailsByPair[$key])) {
|
||||
continue;
|
||||
}
|
||||
$detailsByPair[$key] = [
|
||||
'add_event_id' => $eventId,
|
||||
'user_id' => $userId,
|
||||
'external_userid' => $externalUserId,
|
||||
'add_time' => max(0, (int) ($pair['add_time'] ?? 0)),
|
||||
'is_deleted' => !empty($pair['is_deleted']),
|
||||
'delete_time' => 0,
|
||||
];
|
||||
}
|
||||
|
||||
return array_values($detailsByPair);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按员工聚合全部新增员工+客户组合,并标记其中期末已删除的子集。
|
||||
*
|
||||
* 每个不同 (user_id, external_userid) 都计入 add_fans_count;is_deleted=true 的组合同时计入
|
||||
* deleted_fans_count。deleted_fans_count 只是 add_fans_count 的提示子集,不再扣减或重复累计。
|
||||
*
|
||||
* @param array<int, array<string, mixed>> $candidatePairs
|
||||
* @return array<int, array{user_id: string, add_fans_count: int, deleted_fans_count: int}>
|
||||
*/
|
||||
private static function buildFanCountRows(array $candidatePairs): array
|
||||
{
|
||||
$countsByUser = [];
|
||||
$seenPairs = [];
|
||||
foreach ($candidatePairs as $pair) {
|
||||
$userId = trim((string) ($pair['user_id'] ?? ''));
|
||||
$externalUserId = trim((string) ($pair['external_userid'] ?? ''));
|
||||
if ($userId === '' || $externalUserId === '') {
|
||||
continue;
|
||||
}
|
||||
$key = $userId . "\0" . $externalUserId;
|
||||
if (isset($seenPairs[$key])) {
|
||||
continue;
|
||||
}
|
||||
$seenPairs[$key] = true;
|
||||
$countsByUser[$userId] ??= [
|
||||
'user_id' => $userId,
|
||||
'add_fans_count' => 0,
|
||||
'deleted_fans_count' => 0,
|
||||
];
|
||||
++$countsByUser[$userId]['add_fans_count'];
|
||||
if (!isset($effectiveKeys[$key])) {
|
||||
++$countsByUser[$userId]['deleted_fans_count'];
|
||||
}
|
||||
];
|
||||
++$countsByUser[$userId]['add_fans_count'];
|
||||
if (!empty($pair['is_deleted'])) {
|
||||
++$countsByUser[$userId]['deleted_fans_count'];
|
||||
}
|
||||
}
|
||||
|
||||
return array_values($countsByUser);
|
||||
@@ -1569,25 +1566,25 @@ class ConversionLogic
|
||||
* - add_way=201/202 继承/分配(内部成员共享、管理员/负责人分配,含在职/离职继承)。
|
||||
* 无本地客户档案或跟进信息不含该员工时保守保留(无法判定则仍计加粉)。
|
||||
*
|
||||
* @param array<int, array<string, mixed>> $pairs
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
private static function excludeUncountedFanPairs(array $pairs): array
|
||||
{
|
||||
if ($pairs === []) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$externalUserIds = [];
|
||||
foreach ($pairs as $pair) {
|
||||
$extId = trim((string) ($pair['external_userid'] ?? ''));
|
||||
* @param array<int, array<string, mixed>> $pairs
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
private static function excludeUncountedFanPairs(array $pairs): array
|
||||
{
|
||||
if ($pairs === []) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$externalUserIds = [];
|
||||
foreach ($pairs as $pair) {
|
||||
$extId = trim((string) ($pair['external_userid'] ?? ''));
|
||||
if ($extId !== '') {
|
||||
$externalUserIds[$extId] = true;
|
||||
}
|
||||
}
|
||||
$externalIdList = array_keys($externalUserIds);
|
||||
if ($externalIdList === []) {
|
||||
return $pairs;
|
||||
$externalIdList = array_keys($externalUserIds);
|
||||
if ($externalIdList === []) {
|
||||
return $pairs;
|
||||
}
|
||||
|
||||
/** @var array<string, true> $excludedKeys user_id\0external_userid */
|
||||
@@ -1628,21 +1625,21 @@ class ConversionLogic
|
||||
}
|
||||
}
|
||||
|
||||
if ($excludedKeys === []) {
|
||||
return $pairs;
|
||||
}
|
||||
|
||||
$kept = [];
|
||||
foreach ($pairs as $pair) {
|
||||
$userId = trim((string) ($pair['user_id'] ?? ''));
|
||||
$extId = trim((string) ($pair['external_userid'] ?? ''));
|
||||
if ($excludedKeys === []) {
|
||||
return $pairs;
|
||||
}
|
||||
|
||||
$kept = [];
|
||||
foreach ($pairs as $pair) {
|
||||
$userId = trim((string) ($pair['user_id'] ?? ''));
|
||||
$extId = trim((string) ($pair['external_userid'] ?? ''));
|
||||
if ($userId === '' || $extId === '') {
|
||||
continue;
|
||||
}
|
||||
if (isset($excludedKeys[$userId . "\0" . $extId])) {
|
||||
continue;
|
||||
}
|
||||
$kept[] = $pair;
|
||||
$kept[] = $pair;
|
||||
}
|
||||
|
||||
return $kept;
|
||||
|
||||
Reference in New Issue
Block a user