@@ -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<int,true> $rowDeptSet
* @param string[] |null $mediaSources null=全部渠道;空数组=所选渠道没有可匹配的手工来源
* @param array<string,mixed> |null $mediaChannel null=全部渠道
* @return array{dept:array<int,int>,admin:array<int,int>}
*/
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<string,mixed>|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 ) {