diff --git a/admin/src/views/first_visit/conversion/index.vue b/admin/src/views/first_visit/conversion/index.vue index 9d52ac22c..2a320172d 100644 --- a/admin/src/views/first_visit/conversion/index.vue +++ b/admin/src/views/first_visit/conversion/index.vue @@ -154,7 +154,7 @@

明细数据列表

-

展开部门可查看人员明细;加粉=总进线=区间新增“员工+客户”组合(同一员工的同一客户只计一次,同一客户进入不同员工分别计数;包含区间内添加后已删客户,剔除继承客户、扫一扫/搜手机号/名片分享添加及区间前已存在的相同组合);挂号=已支付且实收低于 10 元的订单,预约=有效预约记录;开口率=开口/加粉,挂号率=挂号/加粉,面诊率=面诊/挂号(看挂号后流失),预约率=面诊/预约(看预约后未面诊),面诊接诊率=接诊诊单/面诊,接诊率=接诊诊单/总进线

+

展开部门可查看人员明细;加粉=总进线=区间新增“员工+客户”组合(同一员工的同一客户只计一次,同一客户进入不同员工分别计数;包含区间内添加后已删客户,剔除继承客户、扫一扫/搜手机号/名片分享添加及区间前已存在的相同组合);开口=获客客户首次发起会话事件,按开口时间计入,同一员工的同一客户只计一次;挂号=已支付且实收低于 10 元的订单,预约=有效预约记录;开口率=开口/加粉,挂号率=挂号/加粉,面诊率=面诊/挂号(看挂号后流失),预约率=面诊/预约(看预约后未面诊),面诊接诊率=接诊诊单/面诊,接诊率=接诊诊单/总进线

{{ dashboard.rows.length }} 个顶层节点
@@ -449,7 +449,7 @@ const timeOptions = [ ] const metricCards: Array<{ key: string; label: string; type: MetricType; hint: string }> = [ { key: 'add_fans_count', label: '加粉数', type: 'count', hint: '区间新增员工+客户组合(不同员工分别计数,含已删除)' }, - { key: 'total_open_count', label: '开口数', type: 'count', hint: '来源于个人业绩录入' }, + { key: 'total_open_count', label: '开口数', type: 'count', hint: '获客客户首次发起会话;按开口时间统计,同一员工的同一客户只计一次' }, { key: 'interview_count', label: '面诊', type: 'count', hint: '已完成预约' }, { key: 'completed_order_count', label: '接诊诊单', type: 'count', hint: '业务订单,按创建人归属并过滤无效单' }, { key: 'completed_order_amount', label: '诊单金额', type: 'money', hint: '排除取消、拒收、退款及已退款订单' }, diff --git a/server/app/adminapi/logic/firstvisit/FirstVisitConversionLogic.php b/server/app/adminapi/logic/firstvisit/FirstVisitConversionLogic.php index 334ce4f05..d9682515c 100644 --- a/server/app/adminapi/logic/firstvisit/FirstVisitConversionLogic.php +++ b/server/app/adminapi/logic/firstvisit/FirstVisitConversionLogic.php @@ -10,7 +10,6 @@ use app\adminapi\logic\stats\ConversionLogic; use app\adminapi\logic\stats\YejiStatsLogic; use app\common\model\auth\Admin; use app\common\model\auth\AdminDept; -use app\common\model\stats\PersonalYeji; use app\common\service\DataScope\DataScopeService; use app\common\service\qywx\MediaChannelService; use think\facade\Db; @@ -18,7 +17,7 @@ use think\facade\Db; /** * 一诊「综合数据转化」。 * - * 自动指标复用 ConversionLogic;开口数来自个人业绩录入。所有筛选先与 DataScope + * 自动指标复用 ConversionLogic;开口数来自获客客户首次发起会话事件。所有筛选先与 DataScope * 可见管理员集合取交集,HTTP 参数不能扩大当前账号的数据范围。 */ class FirstVisitConversionLogic @@ -94,7 +93,7 @@ class FirstVisitConversionLogic $endDate, $effectiveAdminIds, array_fill_keys(array_keys($rowDeptIdSet), true), - self::personalYejiMediaSources($selectedMediaChannelCode, $selectedMediaChannel) + $selectedMediaChannel ); $openDirect = $openCounts['dept']; self::applyOpenCounts($rows, $openDirect, $openCounts['admin']); @@ -159,8 +158,8 @@ class FirstVisitConversionLogic 'selected_media_channel_code' => $selectedMediaChannelCode, 'selected_media_channel_name' => $selectedMediaChannelName, 'open_count_source' => $selectedMediaChannelCode === '' - ? '个人业绩录入' - : '个人业绩录入(按渠道名称匹配)', + ? '企微获客客户首次开口事件' + : '企微获客客户首次开口事件(按当前客户标签筛选渠道)', 'can_view_finance' => $canViewFinance, 'appointment_rule' => '按预约日期统计,归属优先挂号医助、再回退诊单医助;仅含已预约、已完成和已过号', 'registration_rule' => '按支付时间统计已支付且实收金额大于 0、低于 10 元的订单,每笔计 1 个挂号并按订单创建人归属', @@ -713,7 +712,7 @@ class FirstVisitConversionLogic /** * @param int[]|null $effectiveAdminIds * @param array $rowDeptSet - * @param string[]|null $mediaSources null=全部渠道;空数组=所选渠道没有可匹配的手工来源 + * @param array|null $mediaChannel null=全部渠道 * @return array{dept:array,admin:array} */ private static function loadOpenCounts( @@ -721,22 +720,42 @@ class FirstVisitConversionLogic string $endDate, ?array $effectiveAdminIds, array $rowDeptSet, - ?array $mediaSources = null + ?array $mediaChannel = null ): array { - if ($effectiveAdminIds === [] || $rowDeptSet === [] || $mediaSources === []) { + if ($effectiveAdminIds === [] || $rowDeptSet === []) { return ['dept' => [], 'admin' => []]; } - $query = PersonalYeji::whereBetween('yeji_date', [$startDate, $endDate]); + $startTimestamp = strtotime($startDate . ' 00:00:00'); + $endTimestamp = strtotime($endDate . ' 23:59:59'); + $query = Db::name('qywx_customer_acquisition_event') + ->alias('open_event') + ->join( + 'qywx_customer_acquisition_customer acquisition_customer', + 'acquisition_customer.link_id = open_event.link_id' + . ' AND acquisition_customer.external_userid = open_event.external_userid' + . ' AND acquisition_customer.userid = open_event.userid' + ) + ->where('open_event.change_type', 'customer_start_chat') + ->where('open_event.status', 1) + ->whereBetween('open_event.event_time', [$startTimestamp, $endTimestamp]) + ->where('acquisition_customer.chat_status', 1) + ->where('acquisition_customer.owner_admin_id', '>', 0); if ($effectiveAdminIds !== null) { - $query->whereIn('creator_id', $effectiveAdminIds); + $query->whereIn('acquisition_customer.owner_admin_id', $effectiveAdminIds); } - if ($mediaSources !== null) { - $query->whereIn('media_source', $mediaSources); + if ($mediaChannel !== null) { + MediaChannelService::applyExternalUserChannelFilter( + $query, + 'acquisition_customer.external_userid', + $mediaChannel, + 'acquisition_customer.userid' + ); } $rows = $query - ->fieldRaw('creator_id, SUM(total_open_count) AS open_count') - ->group('creator_id') + // 客户可从多个获客链接进入;按员工+客户去重,与加粉组合口径一致。 + ->fieldRaw('acquisition_customer.owner_admin_id AS creator_id, COUNT(DISTINCT acquisition_customer.external_userid) AS open_count') + ->group('acquisition_customer.owner_admin_id') ->select() ->toArray(); if ($rows === []) { @@ -871,44 +890,6 @@ class FirstVisitConversionLogic return $sum; } - /** - * 手工开口按 personal_yeji.media_source 保存;渠道筛选时仅匹配该渠道自身的稳定标识和名称。 - * 不使用 source_group_name,避免同组多个渠道的开口数被重复计入每个渠道。 - * - * @param array|null $channel - * @return string[]|null - */ - private static function personalYejiMediaSources(string $channelCode, ?array $channel): ?array - { - if ($channelCode === '') { - return null; - } - if ($channel === null) { - return []; - } - - $values = [ - $channelCode, - $channel['channel_name'] ?? '', - $channel['source_tag_name'] ?? '', - $channel['legacy_channel_name'] ?? '', - $channel['legacy_source_tag_name'] ?? '', - ]; - foreach (['channel_codes', 'channel_names', 'source_tag_names'] as $listKey) { - if (!isset($channel[$listKey]) || !is_array($channel[$listKey])) { - continue; - } - foreach ($channel[$listKey] as $item) { - $values[] = $item; - } - } - - return array_values(array_unique(array_filter(array_map( - static fn ($value): string => trim((string) $value), - $values - ), static fn (string $value): bool => $value !== '' && !str_starts_with($value, MediaChannelService::GROUP_CODE_PREFIX)))); - } - private static function canViewFinance(int $adminId, array $adminInfo): bool { if ((int) ($adminInfo['root'] ?? 0) === 1) {