@@ -5,6 +5,8 @@ declare(strict_types=1);
namespace app\adminapi\logic\stats ;
use app\adminapi\logic\dept\DeptLogic ;
use app\common\model\auth\AdminDept ;
use app\common\model\tcm\Diagnosis ;
use app\common\model\tcm\PrescriptionOrder ;
use app\common\service\DataScope\DataScopeService ;
use app\common\service\qywx\MediaChannelService ;
@@ -20,10 +22,17 @@ use think\facade\Db;
* · **二中心及其组织下级在选定渠道下计 0**(与进线一致)
* - 复诊统计 = 仅**名称含「二中心」的部门及其组织架构下级**:区间内业务单按患者 create_time 排序,第 1 笔不计,第 2 笔起分列「复诊2」「复诊3」…;口径同业绩单条件;按**订单创建人**所在部门在组织树内 rollup(含下级),与排行榜复诊一致
* - 已完成挂号 = status=3 挂号条数(appointment_date),列「已完成挂号」;部门归属:有效医助→接诊医生→展示行映射。
<<<<<<< HEAD
* - 接诊诊单 = 列名对应**业务订单条数**(非取消、create_time 区间),按**订单创建人**人事部门落在该行子树即计入;多行命中时取**最深的展示部门行**。与处方列表按 assistant_dept_id 筛选不同(列表保留旧口径:诊单医助 ∪ 创建人)。
* - {渠道}成交单 = 选定渠道时与 {渠道}业绩同源:区间内 **非取消业务订单条数**(仅剔除 fulfillment_status=4; NULL 与其它状态计入,按 create_time 归日);dict(channels) 与 tag 口径均按订单计(按订单创建人归属),非挂号条数。
* - 合计业绩 = 区间内业务订单 amount 求和(排除已取消(4) 与软删),**部门列**与「接诊诊单」一致:**订单创建人**人事部门落在该展示行子树则计入,多行命中取最深展示行;与排行榜诊金同口径。
* 无法归入任一展示部门的金额单列「未归属中心」行;**底栏「合计业绩」「接诊诊单」= 当前表内各行业绩列相加**(含未归属行),与弹窗摊分口径一致。
=======
* - 接诊诊单 = 列名对应**业务订单条数**(非取消、create_time 区间),与处方列表按 **assistant_dept_id** 筛选时一致:优先按订单 **creator_id** 人事部门;无创建人时回退诊单 **assistant_id**;多行命中时取**最深的展示部门行**,与「业绩归属医助」分组可能不同。
* - {渠道}成交单 = 选定渠道时与 {渠道}业绩同源:区间内 **非取消业务订单条数**(仅剔除 fulfillment_status=4; NULL 与其它状态计入,按 create_time 归日);dict(channels) 与 tag 口径均按订单计(tag 时按诊单医助收窄),非挂号条数。
* - 合计业绩 = 区间内业务订单 amount 求和(排除已取消(4) 与软删),**部门列**与「接诊诊单」一致;与处方列表按 assistant_dept_id 筛选后的金额合计一致。
* 无法归入任一展示部门的金额单列「未归属中心」行;**全表合计行**仍为同时间/业绩条件的全表 SUM(不限部门)。
>>>>>>> master
* · 时间窗与 stats_order_amount_performance 一致;列表若另有患者筛选或账号数据域收窄,金额可能仍不同
* · 选定渠道时「{渠道}业绩」列仍按比例展示;合计列全量与渠道列含未归属补差;**二中心及其组织下级展示部门不参与该渠道进线/业绩/成交单/ROI 分子**(与同模块 DeptLogic::getErCenterSubtreeDeptIdSet 一致)
* - {渠道}业绩 = 与「处方订单列表」业绩同口径(≠4):按 **o.create_time** 落入区间,按**订单创建人**归部门;
@@ -550,12 +559,20 @@ class YejiStatsLogic
}
$scopedTableNote = ( $explicitDeptFilter
<<<<<<< HEAD
? ' 已筛选展示部门时,表格仅展示所选树中的部门行;底栏「合计业绩」「接诊诊单」与当前表内各行业绩列相加一致(含「未归属中心」行若展示)。'
=======
? ' 已筛选展示部门时,表格「合计业绩」「接诊诊单」各行为同一套列表部门口径(**创建人人事部门优先**,无创建人则诊单医助;∪子树、最深展示行);底栏合计业绩/成交单与处方订单列表 extend(所选部门树+数据域)一致,可能与各行相加不同。'
>>>>>>> master
: '' )
. ( $dataScopeRestricted
? ' 当前账号受数据范围限制时,底栏合计业绩与列表同域。'
: '' );
<<<<<<< HEAD
$baseNoteNoChannel = '业绩口径:除履约已取消(4)外全部计入。部门行「合计业绩」「接诊诊单」「复诊」均按**订单创建人人事部门 ∈ 该展示行子树**归属(多行命中取最深展示行),与医助排行榜诊金一致;与处方订单列表 assistant_dept_id 默认筛选(诊单医助 ∪ 创建人)不同——侧栏点击会自动收窄为创建人口径。多表「本月」区间为当月1日至服务器当日。**投放成本**按进线占比分摊,**二中心子树展示部门进线在分摊中计 0**(与部门 ErCenter 子树一致)。' ;
=======
$baseNoteNoChannel = '业绩口径:除履约已取消(4)外全部计入。部门行「合计业绩」与「接诊诊单」均为列表 assistant_dept_id 口径(**创建人优先**,否则诊单医助;人事部门∪子树,多行命中取最深展示行)。多表「本月」区间为当月1日至服务器当日。**投放成本**按进线占比分摊,**二中心子树展示部门进线在分摊中计 0**(与部门 ErCenter 子树一致)。' ;
>>>>>>> master
return [
'start_date' => $startDate ,
@@ -565,9 +582,15 @@ class YejiStatsLogic
'channel_name' => $channelInfo ? ( string ) $channelInfo [ 'channel_name' ] : '' ,
'channel_group' => $channelInfo ? ( string ) $channelInfo [ 'source_group_name' ] : '' ,
'channel_filter_note' => $channelCode !== ''
<<<<<<< HEAD
? ( '{渠道}业绩:按订单 create_time 归日、仅剔除履约已取消(4)(NULL 与其它状态均计入);渠道判定用"该患者是否有挂号 channels 命中 dict + status=3"(不限挂号时点)。部门归属按**订单创建人**人事部门 ∈ 子树。'
. ' {渠道}成交单数:与{渠道}业绩同源,仅剔除已取消(4)的订单条数(非挂号条数),按订单创建人归部门。'
. ' 「已完成挂号」列同 admin 挂号已完成列表;「接诊诊单」列为业务订单条数(非取消),同样按订单创建人归部门。**二中心及其组织下级**(与部门模块 ErCenter 子树一致)**不计入**该渠道进线、业绩与成交单。**投放成本**按各部门进线条数占展示部门进线合计的比例分摊,**二中心子树进线在分摊中计 0**(未选渠道时仍显示进线数,仅不参与均摊)。ROI = 渠道业绩(不含二中心子树)÷ 总投放成本。进线按所选标签客户精确过滤。'
=======
? ( '{渠道}业绩:与「处方订单列表」业绩同口径,按订单 create_time 归日、仅剔除履约已取消(4)(NULL 与其它状态均计入);渠道判定用"该患者是否有挂号 channels 命中 dict + status=3"(不限挂号时点)。'
. ' {渠道}成交单数:与{渠道}业绩同源,仅剔除已取消(4)的订单条数(非挂号条数)。'
. ' 「已完成挂号」列同 admin 挂号已完成列表;「接诊诊单」列为业务订单条数(非取消),与列表按 assistant_dept_id 筛选时一致(**创建人优先**,否则诊单医助;∪子树)。**二中心及其组织下级**(与部门模块 ErCenter 子树一致)**不计入**该渠道进线、业绩与成交单。**投放成本**按各部门进线条数占展示部门进线合计的比例分摊,**二中心子树进线在分摊中计 0**(未选渠道时仍显示进线数,仅不参与均摊)。ROI = 渠道业绩(不含二中心子树)÷ 总投放成本。进线按所选标签客户精确过滤。'
>>>>>>> master
. ( $tagFallback
? ' ⚠ 当前业务尚未回填 tcm_diagnosis.external_userid 与 user_auth.unionid,已自动退化为「该标签客户的跟进医助 → 区间内接诊/业绩」口径,仅用于趋势观察。'
: '' )
@@ -3399,6 +3422,101 @@ class YejiStatsLogic
}
/**
<<<<<<< HEAD
=======
* 与处方订单列表 buildBaseQueryForStats(仅人事筛选维度)对齐的订单查询 WHERE。
*
* @param int[]|null $expandedDeptIds
* @param int[]|null $visibleAdminIds
*/
private static function applyOrderListStatsVisibilityToPrescriptionQuery (
$query ,
? array $expandedDeptIds ,
? array $visibleAdminIds
) : void {
$hasDept = $expandedDeptIds !== null && $expandedDeptIds !== [];
$hasVis = $visibleAdminIds !== null && $visibleAdminIds !== [];
if ( ! $hasDept && ! $hasVis ) {
return ;
}
$diagTable = self :: tableWithPrefix ( 'tcm_diagnosis' );
if ( $hasDept ) {
$ids = array_values ( array_unique ( array_filter ( array_map ( 'intval' , $expandedDeptIds ), static function ( int $x ) : bool {
return $x > 0 ;
})));
if ( $ids === []) {
$query -> whereRaw ( '0 = 1' );
return ;
}
$inList = implode ( ',' , $ids );
$diagTb = ( new Diagnosis ()) -> getTable ();
$adTb = ( new AdminDept ()) -> getTable ();
$query -> whereRaw ( self :: sqlPrescriptionOrderListDeptSubtreeMatchCreatorFirst ( '`o`' , $diagTb , $adTb , $inList ));
}
if ( $hasVis ) {
$ids = array_values ( array_unique ( array_filter ( array_map ( 'intval' , $visibleAdminIds ), static function ( int $x ) : bool {
return $x > 0 ;
})));
if ( $ids === []) {
$query -> whereRaw ( '0 = 1' );
return ;
}
$inList = implode ( ',' , $ids );
$query -> where ( function ( $q ) use ( $diagTable , $inList , $ids ) {
$q -> whereIn ( 'o.creator_id' , $ids );
$q -> whereOrRaw (
" EXISTS (SELECT 1 FROM { $diagTable } dg WHERE dg.id = o.diagnosis_id "
. " AND dg.delete_time IS NULL AND dg.assistant_id IN ( { $inList } )) "
);
});
}
}
/**
* 收窄合计时与订单列表 extend「业绩金额」一致(create_time、NOT(fulfillment<=>4)、人事筛选)。
*
* @param int[]|null $expandedDeptIds
* @param int[]|null $visibleAdminIds
*/
private static function sumOrderListAlignedPerformanceAmount (
int $startTs ,
int $endTs ,
? array $expandedDeptIds ,
? array $visibleAdminIds
) : float {
$query = Db :: name ( 'tcm_prescription_order' )
-> alias ( 'o' )
-> whereNull ( 'o.delete_time' )
-> where ( 'o.create_time' , 'between' , [ $startTs , $endTs ]);
self :: applyPrescriptionOrderNotCancelledForPerformance ( $query );
self :: applyOrderListStatsVisibilityToPrescriptionQuery ( $query , $expandedDeptIds , $visibleAdminIds );
return round (( float ) $query -> sum ( 'o.amount' ), 1 );
}
/**
* 与 sumOrderListAlignedPerformanceAmount 同源,计订单条数。
*/
private static function countOrderListAlignedPerformanceOrders (
int $startTs ,
int $endTs ,
? array $expandedDeptIds ,
? array $visibleAdminIds
) : int {
$query = Db :: name ( 'tcm_prescription_order' )
-> alias ( 'o' )
-> whereNull ( 'o.delete_time' )
-> where ( 'o.create_time' , 'between' , [ $startTs , $endTs ]);
self :: applyPrescriptionOrderNotCancelledForPerformance ( $query );
self :: applyOrderListStatsVisibilityToPrescriptionQuery ( $query , $expandedDeptIds , $visibleAdminIds );
return ( int ) $query -> count ();
}
/**
>>>>>>> master
* 标签/医助兜底口径下的渠道向业绩全量 SUM(与 sumPerformance 的 scoped 分支同一组 WHERE,无部门归属过滤)。
*/
private static function sumGlobalPerformanceTagScoped (
@@ -3824,8 +3942,13 @@ class YejiStatsLogic
}
/**
<<<<<<< HEAD
* 与业绩看板部门列一致:仅按订单创建人候选,将其人事部门落在展示行子树即命中;命中多行时取组织深度最大的展示行。
* (处方订单列表默认 assistant_dept_id 仍为「诊单医助 ∪ 创建人」并集,与看板有别。)
=======
* 与 {@see PrescriptionOrderLists::applyDoctorAssistantFilters} 中 assistant_dept_id(未开抽屉对齐开关)一致:
* 有创建人则仅用创建人人事部门落在该行子树即命中;无创建人则诊单医助;命中多行时取组织深度最大的展示行。
>>>>>>> master
*
* @param array<int, true> $candidates admin_id => true
* @param array<int, int[]> $adminDeptIdList
@@ -3899,8 +4022,12 @@ class YejiStatsLogic
}
/**
<<<<<<< HEAD
* 看板「合计业绩 / 接诊诊单」列用的部门摊行:仅按订单创建人 o.creator_id 的人事部门映射到展示行子树(命中多行取最深),
* 与侧栏 yeji_order_drawer=1、{@see sqlPerformanceAttributionAdminExpr} 一致。不再把诊单医助纳入归属候选。
=======
* 列表部门口径(有创建人则仅用创建人、否则诊单医助 ⇄ 展示行子树,最深行):聚单笔数与金额,供「接诊诊单」与「合计业绩」列共用。
>>>>>>> master
*
* @return array{count: array<int, int>, amount: array<int, float>}
*/
@@ -3970,6 +4097,8 @@ class YejiStatsLogic
$c = ( int ) ( $r [ 'creator_id' ] ? ? 0 );
if ( $c > 0 ) {
$adminIds [] = $c ;
} elseif ( $a > 0 ) {
$adminIds [] = $a ;
}
}
$adminIds = array_values ( array_unique ( array_filter ( $adminIds , static fn ( int $x ) : bool => $x > 0 )));
@@ -3990,8 +4119,16 @@ class YejiStatsLogic
$byAmt = [];
foreach ( $orderRows as $r ) {
$cr = ( int ) ( $r [ 'creator_id' ] ? ? 0 );
<<<<<<< HEAD
if ( $cr <= 0 ) {
continue ;
=======
$cand = [];
if ( $cr > 0 ) {
$cand [ $cr ] = true ;
} elseif ( $asst > 0 ) {
$cand [ $asst ] = true ;
>>>>>>> master
}
$cand = [ $cr => true ];
$deptKey = self :: resolveDeepestYejiTableRowForListDeptOr (
@@ -4014,9 +4151,15 @@ class YejiStatsLogic
}
/**
<<<<<<< HEAD
* 成交单数按部门:有展示行时与 {@see aggregateListAlignedOrderMetricsByDept} 同口径(仅订单创建人归属)。
* 无展示行时按 sqlPerformanceAttributionAdminExpr 分组再 fold(同样为创建人)。
* 选定医助 tag 时收窄为 o.creator_id ∈ 集。
=======
* 成交单数按部门:**处方订单列表**按 assistant_dept_id 筛选时的业务单条数(创建人优先、无创建人则诊单医助 ∪ 部门子树),
* 与 {@see PrescriptionOrderLists::applyDoctorAssistantFilters} 在 <b>未</b> 传 yeji_drawer_match_table_performance 时一致。
* 选定医助 tag 时收窄为(业绩归属医助 ∈ 集 **或** 诊单医助 ∈ 集 **或** 创建人 ∈ 集)。
>>>>>>> master
*
* @return array<int, int>
*/
@@ -4804,6 +4947,7 @@ class YejiStatsLogic
}
/**
<<<<<<< HEAD
* 提成结算统计:渠道收窄条件应用到业务订单查询(与 sumPerformance「scoped」同源)。
*/
private static function applyCommissionSettlementChannelFilter ( Query $query , array $c ) : void
@@ -4837,11 +4981,49 @@ class YejiStatsLogic
return ;
}
if ( $tagAssistantIds !== null && $tagAssistantIds === [] && $tagFallback ) {
=======
* 业务订单列表 WHERE 片段:有创建人则仅按创建人人事部门落在子树;无创建人则按诊单医助(与
* { @ see aggregateListAlignedOrderMetricsByDept } 及
* { @ see PrescriptionOrderLists :: applyDoctorAssistantFilters } 中 assistant_dept_id 一致)。
*
* @ param string $orderRefBackticked 主查询里业务订单表标识,须已加反引号:列表 / Model 无别名时用 `` `zyt_tcm_prescription_order` `` ; Db:: name () -> alias ( 'o' ) 时用 `` `o` ``
* @ param string $inListComma 已净化的 id 列表(仅数字与逗号)
*/
public static function sqlPrescriptionOrderListDeptSubtreeMatchCreatorFirst (
string $orderRefBackticked ,
string $diagTbl ,
string $adTbl ,
string $inListComma
) : string {
$creatorInSubtree = " EXISTS (SELECT 1 FROM ` { $adTbl } ` adc WHERE adc.`admin_id` = { $orderRefBackticked } .`creator_id` AND adc.`dept_id` IN ( { $inListComma } )) " ;
$assistantInSubtree = " EXISTS (SELECT 1 FROM ` { $adTbl } ` ad INNER JOIN ` { $diagTbl } ` dg ON ad.admin_id = dg.assistant_id AND dg.delete_time IS NULL "
. " WHERE dg.`id` = { $orderRefBackticked } .`diagnosis_id` AND ad.dept_id IN ( { $inListComma } )) " ;
return '('
. '((IFNULL(' . $orderRefBackticked . '.`creator_id`,0) > 0) AND (' . $creatorInSubtree . '))'
. ' OR '
. '((IFNULL(' . $orderRefBackticked . '.`creator_id`,0) <= 0) AND (' . $assistantInSubtree . '))'
. ')' ;
}
/**
* 业绩看板侧栏:与「接诊诊单」列 {@see aggregateListAlignedOrderMetricsByDept} 同源——在**同一表格**
* 展示行集合内,创建人优先归属;无创建人则医助;多行命中时取组织深度最大的展示行(同深取较小 dept id)。
* 传入当前表全部展示行 dept id。
*
* @param int[] $tableRowDeptIds 当前业绩表 body 行 dept_id(含点击行及兄弟行)
*/
public static function applyPrescriptionOrderListAlignedWinnerDeptFilter ( Query $query , int $targetRowDeptId , array $tableRowDeptIds ) : void
{
$targetRowDeptId = ( int ) $targetRowDeptId ;
if ( $targetRowDeptId <= 0 ) {
>>>>>>> master
$query -> whereRaw ( '0 = 1' );
return ;
}
<<<<<<< HEAD
$att = self :: sqlPerformanceAttributionAdminExpr ();
$narrowed = false ;
if ( $tagDiagIds !== null ) {
@@ -5954,5 +6136,63 @@ class YejiStatsLogic
'next_settlement_month' => $next ,
'deferred_carry_count' => count ( $deferredIds ),
];
=======
$uniq = [];
foreach ( $tableRowDeptIds as $id ) {
$id = ( int ) $id ;
if ( $id > 0 ) {
$uniq [ $id ] = true ;
}
}
$tableRowDeptIds = array_keys ( $uniq );
if ( $tableRowDeptIds === []) {
$query -> whereRaw ( '0 = 1' );
return ;
}
$deptRows = Db :: name ( 'dept' ) -> whereNull ( 'delete_time' ) -> field ([ 'id' , 'pid' ]) -> select () -> toArray ();
$deptById = [];
foreach ( $deptRows as $row ) {
$deptById [( int ) $row [ 'id' ]] = $row ;
}
$dT = self :: deptDepthFromRoot ( $targetRowDeptId , $deptById );
$beaters = [];
foreach ( $tableRowDeptIds as $r ) {
$r = ( int ) $r ;
if ( $r <= 0 || $r === $targetRowDeptId ) {
continue ;
}
$dR = self :: deptDepthFromRoot ( $r , $deptById );
if ( $dR > $dT || ( $dR === $dT && $r < $targetRowDeptId )) {
$beaters [] = $r ;
}
}
$poRef = '`' . str_replace ( '`' , '' , ( new PrescriptionOrder ()) -> getTable ()) . '`' ;
$diagTbl = ( new Diagnosis ()) -> getTable ();
$adTbl = ( new AdminDept ()) -> getTable ();
$tSubtree = DeptLogic :: getSelfAndDescendantIds ( $targetRowDeptId );
$tSubtree = array_values ( array_filter ( array_map ( 'intval' , $tSubtree ), static fn ( int $id ) : bool => $id > 0 ));
if ( $tSubtree === []) {
$query -> whereRaw ( '0 = 1' );
return ;
}
$tIn = implode ( ',' , $tSubtree );
$clauses = [ self :: sqlPrescriptionOrderListDeptSubtreeMatchCreatorFirst ( $poRef , $diagTbl , $adTbl , $tIn )];
foreach ( $beaters as $b ) {
$bSubtree = DeptLogic :: getSelfAndDescendantIds (( int ) $b );
$bSubtree = array_values ( array_filter ( array_map ( 'intval' , $bSubtree ), static fn ( int $id ) : bool => $id > 0 ));
if ( $bSubtree === []) {
continue ;
}
$bIn = implode ( ',' , $bSubtree );
$clauses [] = 'NOT (' . self :: sqlPrescriptionOrderListDeptSubtreeMatchCreatorFirst ( $poRef , $diagTbl , $adTbl , $bIn ) . ')' ;
}
$query -> whereRaw ( '(' . implode ( ' AND ' , $clauses ) . ')' );
>>>>>>> master
}
}