更新
This commit is contained in:
@@ -655,6 +655,7 @@ class ConversionLogic
|
||||
'id' => $id,
|
||||
'name' => $name,
|
||||
'add_fans_count' => 0,
|
||||
'deleted_fans_count' => 0,
|
||||
'total_open_count' => 0, // TODO: 接入 IM 会话统计后回填
|
||||
'unreplied_count' => 0, // TODO: 接入 IM 会话统计后回填
|
||||
'paid_appointment_count' => 0,
|
||||
@@ -924,11 +925,13 @@ class ConversionLogic
|
||||
$userId = (string)($row['user_id'] ?? '');
|
||||
$adminId = (int)($adminByUserId[$userId]['id'] ?? 0);
|
||||
$addFansCount = (int)($row['add_fans_count'] ?? 0);
|
||||
$deletedFansCount = (int)($row['deleted_fans_count'] ?? 0);
|
||||
|
||||
if ($dimension === 'dept' && $adminId <= 0) {
|
||||
// -1 桶仅在 SCOPE_ALL(visibleAdminIds === null)时存在;其他范围下未绑定 admin 的加粉不可归属,跳过。
|
||||
if ($visibleAdminIds === null && isset($entities[self::VIRTUAL_DEPT_UNBOUND_ADMIN_ID])) {
|
||||
$entities[self::VIRTUAL_DEPT_UNBOUND_ADMIN_ID]['add_fans_count'] += $addFansCount;
|
||||
$entities[self::VIRTUAL_DEPT_UNBOUND_ADMIN_ID]['deleted_fans_count'] += $deletedFansCount;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@@ -945,6 +948,7 @@ class ConversionLogic
|
||||
}
|
||||
if (isset($entities[self::VIRTUAL_DEPT_UNASSIGNED_ID])) {
|
||||
$entities[self::VIRTUAL_DEPT_UNASSIGNED_ID]['add_fans_count'] += $addFansCount;
|
||||
$entities[self::VIRTUAL_DEPT_UNASSIGNED_ID]['deleted_fans_count'] += $deletedFansCount;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@@ -955,27 +959,28 @@ class ConversionLogic
|
||||
|
||||
foreach ($hitEntityIds as $entityId) {
|
||||
$entities[$entityId]['add_fans_count'] += $addFansCount;
|
||||
$entities[$entityId]['deleted_fans_count'] += $deletedFansCount;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 区间有效加粉:按企微员工聚合后,再投影到部门/成员/虚拟桶。
|
||||
* 区间新增加粉:按企微员工聚合后,再投影到部门/成员/虚拟桶。
|
||||
*
|
||||
* 口径(对齐企微客户列表 / 官方「新增客户」不含继承,而非原始回调条数):
|
||||
* - 统计区间内 add_external_contact,按 (user_id, external_userid) 去重;
|
||||
* - 同一员工在区间开始前已加过该客户的重加不计(企微「添加时间」仍是首次跟进时间,
|
||||
* 删后再加会再推 add_external_contact,但不能当当天新客,否则会跨日重复计);
|
||||
* - add_external_contact 是企微确认客户关系已建立后的权威事件;会话存档同意
|
||||
* msg_audit_approved 属于独立能力,不能作为加粉前置条件,否则未开通会话存档的员工会被整批清零;
|
||||
* - 加粉之后、统计结束前若出现 del_external_contact(客户从企微侧删除)则不计。
|
||||
* 不扣减 del_follow_user:企微客户列表中改名带「删」的客户往往仍在列表中;
|
||||
* - add_external_contact 是企微确认客户关系已建立后的权威事件;会话存档同意
|
||||
* msg_audit_approved 属于独立能力,不能作为加粉前置条件,否则未开通会话存档的员工会被整批清零;
|
||||
* - 加粉之后、统计结束前若出现 del_external_contact(客户从企微侧删除),仍计入加粉总数,
|
||||
* 并额外计入已删提示子集;不处理 del_follow_user:企微客户列表中改名带「删」的客户往往仍在列表中;
|
||||
* - 剔除非投放加粉:跟进人 add_way∈{1 扫一扫, 2 搜索手机号, 3 名片分享};
|
||||
* - 剔除继承客户:跟进人 add_way∈{201 内部成员共享, 202 管理员/负责人分配}(含在职/离职继承)。
|
||||
*
|
||||
* @param array<string, mixed>|null $mediaChannel
|
||||
* @param int[]|null $adminIds null means all active/unbound WeCom users
|
||||
* @return array<int, array{user_id: string, add_fans_count: int|string}>
|
||||
* @return array<int, array{user_id: string, add_fans_count: int, deleted_fans_count: int}>
|
||||
*/
|
||||
private static function loadFanRows(
|
||||
int $startTimestamp,
|
||||
@@ -994,7 +999,7 @@ class ConversionLogic
|
||||
}
|
||||
}
|
||||
|
||||
$baseKey = self::requestRowsCacheKey('fans-effective-v7', [
|
||||
$baseKey = self::requestRowsCacheKey('fans-effective-v9', [
|
||||
$startTimestamp,
|
||||
$endTimestamp,
|
||||
self::mediaChannelCacheKey($mediaChannel),
|
||||
@@ -1036,18 +1041,9 @@ class ConversionLogic
|
||||
$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 . '` del_e'
|
||||
. ' WHERE del_e.user_id = e.user_id'
|
||||
. ' AND del_e.external_userid = e.external_userid'
|
||||
. ' AND del_e.change_type = ?'
|
||||
. ' AND del_e.event_time >= e.event_time'
|
||||
. ' AND del_e.event_time <= ?)',
|
||||
['del_external_contact', $endTimestamp]
|
||||
)
|
||||
->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'
|
||||
@@ -1061,35 +1057,109 @@ class ConversionLogic
|
||||
if ($workWechatUserIds !== null) {
|
||||
$query->whereIn('e.user_id', $workWechatUserIds);
|
||||
}
|
||||
$effectiveQuery = clone $query;
|
||||
if ($mediaChannel !== null) {
|
||||
MediaChannelService::applyExternalUserChannelFilter($query, 'e.external_userid', $mediaChannel);
|
||||
// 有效加粉保持原口径:只按未删客户的当前标签/渠道关系筛选。
|
||||
MediaChannelService::applyExternalUserChannelFilter($effectiveQuery, 'e.external_userid', $mediaChannel);
|
||||
}
|
||||
$effectivePairs = $effectiveQuery
|
||||
->whereRaw(
|
||||
'NOT EXISTS (SELECT 1 FROM `' . $eventTable . '` del_e'
|
||||
. ' WHERE del_e.user_id = e.user_id'
|
||||
. ' AND del_e.external_userid = e.external_userid'
|
||||
. ' AND del_e.change_type = ?'
|
||||
. ' AND del_e.event_time >= e.event_time'
|
||||
. ' AND del_e.event_time <= ?)',
|
||||
['del_external_contact', $endTimestamp]
|
||||
)
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
$pairs = $query->select()->toArray();
|
||||
$pairs = self::excludeUncountedFanPairs($pairs);
|
||||
|
||||
$countsByUser = [];
|
||||
foreach ($pairs as $pair) {
|
||||
$userId = trim((string) ($pair['user_id'] ?? ''));
|
||||
if ($userId === '') {
|
||||
continue;
|
||||
}
|
||||
$countsByUser[$userId] = ($countsByUser[$userId] ?? 0) + 1;
|
||||
$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();
|
||||
|
||||
$rows = [];
|
||||
foreach ($countsByUser as $userId => $count) {
|
||||
$rows[] = [
|
||||
'user_id' => $userId,
|
||||
'add_fans_count' => $count,
|
||||
];
|
||||
}
|
||||
// add_way 筛选对有效与已删粉丝使用同一口径。
|
||||
$candidatePairs = array_merge($effectivePairs, $deletedPairs);
|
||||
$candidatePairs = self::excludeUncountedFanPairs($candidatePairs);
|
||||
$rows = self::buildFanCountRows($candidatePairs, $effectivePairs);
|
||||
|
||||
self::$requestRowsCache[$cacheKey] = $rows;
|
||||
|
||||
return self::$requestRowsCache[$cacheKey];
|
||||
}
|
||||
|
||||
/**
|
||||
* 按员工聚合全部候选新增,并标记其中期末已删除的子集。
|
||||
*
|
||||
* 候选对已经过“区间前未添加”、渠道与 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] ??= [
|
||||
'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'];
|
||||
}
|
||||
}
|
||||
|
||||
return array_values($countsByUser);
|
||||
}
|
||||
|
||||
/**
|
||||
* 剔除不应计入加粉的跟进来源:
|
||||
* - add_way=1 扫一扫(客户通过扫一扫添加);
|
||||
@@ -1738,6 +1808,7 @@ class ConversionLogic
|
||||
$appointmentTotalCount = (int)($entity['appointment_total_count'] ?? 0);
|
||||
$interviewCount = (int)$entity['interview_count'];
|
||||
$addFansCount = (int)$entity['add_fans_count'];
|
||||
$deletedFansCount = (int)($entity['deleted_fans_count'] ?? 0);
|
||||
$totalOpenCount = (int)$entity['total_open_count'];
|
||||
$completedOrderCount = (int)$entity['completed_order_count'];
|
||||
$businessOrderAmount = round((float)$entity['business_order_amount'], 2);
|
||||
@@ -1746,6 +1817,7 @@ class ConversionLogic
|
||||
$effectiveAccountCost = self::allocateAccountCost($globalAccountCost, $globalAddFansCount, $addFansCount, $isEligibleForAccountCost);
|
||||
|
||||
$entity['paid_appointment_count'] = $paidAppointmentCount;
|
||||
$entity['deleted_fans_count'] = $deletedFansCount;
|
||||
$entity['free_appointment_count'] = $freeAppointmentCount;
|
||||
$entity['appointment_total_count'] = $appointmentTotalCount;
|
||||
$entity['business_order_amount'] = $businessOrderAmount;
|
||||
@@ -1830,6 +1902,7 @@ class ConversionLogic
|
||||
$childNode = $buildNode((int)$childId);
|
||||
$children[] = $childNode;
|
||||
$node['add_fans_count'] += (int)$childNode['add_fans_count'];
|
||||
$node['deleted_fans_count'] += (int)($childNode['deleted_fans_count'] ?? 0);
|
||||
$node['total_open_count'] += (int)$childNode['total_open_count'];
|
||||
$node['unreplied_count'] += (int)$childNode['unreplied_count'];
|
||||
$node['paid_appointment_count'] += (int)$childNode['paid_appointment_count'];
|
||||
@@ -1972,6 +2045,7 @@ class ConversionLogic
|
||||
$appointmentTotalCount = (int)($node['appointment_total_count'] ?? 0);
|
||||
$interviewCount = (int)$node['interview_count'];
|
||||
$addFansCount = (int)$node['add_fans_count'];
|
||||
$deletedFansCount = (int)($node['deleted_fans_count'] ?? 0);
|
||||
$totalOpenCount = (int)$node['total_open_count'];
|
||||
$completedOrderCount = (int)$node['completed_order_count'];
|
||||
$businessOrderAmount = round((float)$node['business_order_amount'], 2);
|
||||
@@ -1984,6 +2058,7 @@ class ConversionLogic
|
||||
}
|
||||
|
||||
$node['paid_appointment_count'] = $paidAppointmentCount;
|
||||
$node['deleted_fans_count'] = $deletedFansCount;
|
||||
$node['free_appointment_count'] = $freeAppointmentCount;
|
||||
$node['appointment_total_count'] = $appointmentTotalCount;
|
||||
$node['business_order_amount'] = $businessOrderAmount;
|
||||
@@ -2103,7 +2178,7 @@ class ConversionLogic
|
||||
}
|
||||
foreach ($doctorEntities as $adminId => $entity) {
|
||||
if (isset($combined[$adminId])) {
|
||||
foreach (['add_fans_count', 'total_open_count', 'unreplied_count', 'paid_appointment_count', 'free_appointment_count', 'appointment_total_count', 'interview_count', 'completed_order_count'] as $intKey) {
|
||||
foreach (['add_fans_count', 'deleted_fans_count', 'total_open_count', 'unreplied_count', 'paid_appointment_count', 'free_appointment_count', 'appointment_total_count', 'interview_count', 'completed_order_count'] as $intKey) {
|
||||
$combined[$adminId][$intKey] = (int)($combined[$adminId][$intKey] ?? 0) + (int)($entity[$intKey] ?? 0);
|
||||
}
|
||||
foreach (['business_order_amount', 'completed_order_amount'] as $moneyKey) {
|
||||
@@ -2277,7 +2352,8 @@ class ConversionLogic
|
||||
foreach ($rows as $row) {
|
||||
$userId = (string)($row['user_id'] ?? '');
|
||||
$addFansCount = (int)($row['add_fans_count'] ?? 0);
|
||||
if ($userId === '' || $addFansCount <= 0) {
|
||||
$deletedFansCount = (int)($row['deleted_fans_count'] ?? 0);
|
||||
if ($userId === '' || ($addFansCount <= 0 && $deletedFansCount <= 0)) {
|
||||
continue;
|
||||
}
|
||||
$resolvedName = $nameByUserId[$userId] ?? '';
|
||||
@@ -2289,6 +2365,7 @@ class ConversionLogic
|
||||
'未绑定 admin'
|
||||
);
|
||||
$memberRow['add_fans_count'] = $addFansCount;
|
||||
$memberRow['deleted_fans_count'] = $deletedFansCount;
|
||||
$result[] = $memberRow;
|
||||
}
|
||||
|
||||
@@ -2327,7 +2404,8 @@ class ConversionLogic
|
||||
$admin = $adminByUserId[$userId] ?? null;
|
||||
$adminId = (int)($admin['id'] ?? 0);
|
||||
$addFansCount = (int)($row['add_fans_count'] ?? 0);
|
||||
if ($adminId <= 0 || $addFansCount <= 0) {
|
||||
$deletedFansCount = (int)($row['deleted_fans_count'] ?? 0);
|
||||
if ($adminId <= 0 || ($addFansCount <= 0 && $deletedFansCount <= 0)) {
|
||||
continue;
|
||||
}
|
||||
if (isset($assignedAdminIds[$adminId])) {
|
||||
@@ -2345,6 +2423,7 @@ class ConversionLogic
|
||||
);
|
||||
$memberRow['admin_id'] = $adminId;
|
||||
$memberRow['add_fans_count'] = $addFansCount;
|
||||
$memberRow['deleted_fans_count'] = $deletedFansCount;
|
||||
$result[] = $memberRow;
|
||||
}
|
||||
|
||||
@@ -2401,6 +2480,7 @@ class ConversionLogic
|
||||
'role' => $role,
|
||||
'is_leader' => false,
|
||||
'add_fans_count' => 0,
|
||||
'deleted_fans_count' => 0,
|
||||
'total_open_count' => 0,
|
||||
'unreplied_count' => 0,
|
||||
'paid_appointment_count' => 0,
|
||||
@@ -2448,6 +2528,7 @@ class ConversionLogic
|
||||
$appointmentTotalCount = (int)($entity['appointment_total_count'] ?? 0);
|
||||
$interviewCount = (int)($entity['interview_count'] ?? 0);
|
||||
$addFansCount = (int)($entity['add_fans_count'] ?? 0);
|
||||
$deletedFansCount = (int)($entity['deleted_fans_count'] ?? 0);
|
||||
$totalOpenCount = (int)($entity['total_open_count'] ?? 0);
|
||||
$completedOrderCount = (int)($entity['completed_order_count'] ?? 0);
|
||||
$businessOrderAmount = round((float)($entity['business_order_amount'] ?? 0), 2);
|
||||
@@ -2463,6 +2544,7 @@ class ConversionLogic
|
||||
'role' => $role,
|
||||
'is_leader' => $isLeader,
|
||||
'add_fans_count' => $addFansCount,
|
||||
'deleted_fans_count' => $deletedFansCount,
|
||||
'total_open_count' => $totalOpenCount,
|
||||
'unreplied_count' => (int)($entity['unreplied_count'] ?? 0),
|
||||
'paid_appointment_count' => $paidAppointmentCount,
|
||||
@@ -2625,6 +2707,7 @@ class ConversionLogic
|
||||
{
|
||||
$summary = [
|
||||
'add_fans_count' => 0,
|
||||
'deleted_fans_count' => 0,
|
||||
'total_open_count' => 0,
|
||||
'unreplied_count' => 0,
|
||||
'paid_appointment_count' => 0,
|
||||
@@ -2638,6 +2721,7 @@ class ConversionLogic
|
||||
];
|
||||
foreach ($rows as $row) {
|
||||
$summary['add_fans_count'] += (int)$row['add_fans_count'];
|
||||
$summary['deleted_fans_count'] += (int)($row['deleted_fans_count'] ?? 0);
|
||||
$summary['total_open_count'] += (int)$row['total_open_count'];
|
||||
$summary['unreplied_count'] += (int)$row['unreplied_count'];
|
||||
$summary['paid_appointment_count'] += (int)$row['paid_appointment_count'];
|
||||
@@ -2775,6 +2859,7 @@ class ConversionLogic
|
||||
private static function hasBusinessMetrics(array $row): bool
|
||||
{
|
||||
return (int)($row['add_fans_count'] ?? 0) > 0
|
||||
|| (int)($row['deleted_fans_count'] ?? 0) > 0
|
||||
|| (int)($row['paid_appointment_count'] ?? 0) > 0
|
||||
|| (int)($row['free_appointment_count'] ?? 0) > 0
|
||||
|| (int)($row['appointment_total_count'] ?? 0) > 0
|
||||
|
||||
Reference in New Issue
Block a user