This commit is contained in:
Your Name
2026-08-12 11:03:28 +08:00
parent 09d3fcbf82
commit bc9ad1d3cd
32 changed files with 4586 additions and 2341 deletions
File diff suppressed because it is too large Load Diff
@@ -957,8 +957,13 @@ class ConversionLogic
}
/**
* Aggregate add-contact events by WeCom user once, then project that raw
* snapshot to departments, members and virtual buckets in PHP.
* 区间有效加粉:按企微员工聚合后,再投影到部门/成员/虚拟桶。
*
* 口径(对齐企微客户列表,而非原始回调条数):
* - 统计区间内 add_external_contact,按 (user_id, external_userid) 去重;
* - 加粉之后、统计结束前须有 msg_audit_approved(排除未完成链路的幽灵事件);
* - 加粉之后、统计结束前若出现 del_external_contact(客户从企微侧删除)则不计。
* 不扣减 del_follow_user:企微客户列表中改名带「删」的客户往往仍在列表中。
*
* @param array<string, mixed>|null $mediaChannel
* @param int[]|null $adminIds null means all active/unbound WeCom users
@@ -981,7 +986,7 @@ class ConversionLogic
}
}
$baseKey = self::requestRowsCacheKey('fans', [
$baseKey = self::requestRowsCacheKey('fans-effective-v2', [
$startTimestamp,
$endTimestamp,
self::mediaChannelCacheKey($mediaChannel),
@@ -1019,11 +1024,30 @@ 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])
->fieldRaw('e.user_id, COUNT(*) AS add_fans_count')
->whereRaw(
'EXISTS (SELECT 1 FROM `' . $eventTable . '` audit_e'
. ' WHERE audit_e.user_id = e.user_id'
. ' AND audit_e.external_userid = e.external_userid'
. ' AND audit_e.change_type = ?'
. ' AND audit_e.event_time >= e.event_time'
. ' AND audit_e.event_time <= ?)',
['msg_audit_approved', $endTimestamp]
)
->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]
)
->fieldRaw('e.user_id, COUNT(DISTINCT e.external_userid) AS add_fans_count')
->group('e.user_id');
if ($workWechatUserIds !== null) {
$query->whereIn('e.user_id', $workWechatUserIds);
@@ -1604,6 +1628,8 @@ class ConversionLogic
$entity['interview_rate'] = self::percent($interviewCount, $appointmentTotalCount);
$entity['receive_rate'] = self::receiveRate($completedOrderCount, $addFansCount, $interviewCount, $dimension);
$entity['interview_receive_rate'] = self::percent($completedOrderCount, $interviewCount);
// 面诊接诊率:面诊 / 挂号
$entity['interview_paid_rate'] = self::percent($interviewCount, $paidAppointmentCount);
$entity['open_receive_rate'] = self::percent($completedOrderCount, $totalOpenCount);
$entity['avg_unit_price'] = self::safeDivideMoney($completedOrderAmount, $completedOrderCount);
$entity['cash_cost'] = self::safeDivideMoney($effectiveAccountCost, $addFansCount);
@@ -1837,6 +1863,7 @@ class ConversionLogic
$node['interview_rate'] = self::percent($interviewCount, $appointmentTotalCount);
$node['receive_rate'] = self::receiveRate($completedOrderCount, $addFansCount, $interviewCount, 'dept');
$node['interview_receive_rate'] = self::percent($completedOrderCount, $interviewCount);
$node['interview_paid_rate'] = self::percent($interviewCount, $paidAppointmentCount);
$node['open_receive_rate'] = self::percent($completedOrderCount, $totalOpenCount);
$node['avg_unit_price'] = self::safeDivideMoney($completedOrderAmount, $completedOrderCount);
$node['cash_cost'] = self::safeDivideMoney($effectiveAccountCost, $addFansCount);
@@ -2258,6 +2285,7 @@ class ConversionLogic
'interview_rate' => 0.0,
'receive_rate' => 0.0,
'interview_receive_rate' => 0.0,
'interview_paid_rate' => 0.0,
'open_receive_rate' => 0.0,
'avg_unit_price' => 0.0,
'cash_cost' => 0.0,
@@ -2324,6 +2352,7 @@ class ConversionLogic
'member'
),
'interview_receive_rate' => self::percent($completedOrderCount, $interviewCount),
'interview_paid_rate' => self::percent($interviewCount, $paidAppointmentCount),
'open_receive_rate' => self::percent($completedOrderCount, $totalOpenCount),
'avg_unit_price' => self::safeDivideMoney($completedOrderAmount, $completedOrderCount),
'cash_cost' => self::safeDivideMoney($effectiveAccountCost, $addFansCount),
@@ -2500,6 +2529,7 @@ class ConversionLogic
$dimension
);
$summary['interview_receive_rate'] = self::percent($summary['completed_order_count'], $summary['interview_count']);
$summary['interview_paid_rate'] = self::percent($summary['interview_count'], $summary['paid_appointment_count']);
$summary['open_receive_rate'] = self::percent($summary['completed_order_count'], $summary['total_open_count']);
$summary['avg_unit_price'] = self::safeDivideMoney($summary['completed_order_amount'], $summary['completed_order_count']);
$summary['cash_cost'] = self::safeDivideMoney($summary['account_cost'], $summary['add_fans_count']);