This commit is contained in:
Your Name
2026-08-06 14:48:14 +08:00
parent d5b0ab4709
commit a010483bdc
337 changed files with 474 additions and 320 deletions
@@ -42,6 +42,8 @@ class ConversionLogic
$includeFilters = (int)($params['include_filters'] ?? 0) === 1;
// 仅供需要“有效挂号”口径的内部看板调用;默认保持转换统计历史口径不变。
$excludeCancelledAppointments = (int)($params['exclude_cancelled_appointments'] ?? 0) === 1;
// 一诊综合转化复用处方订单页的业绩口径;其它调用方继续保留历史“双审完成单”口径。
$usePerformanceOrderMetrics = strtolower(trim((string)($params['order_metric_mode'] ?? ''))) === 'performance';
$dimension = self::normalizeDimension((string)($params['dimension'] ?? 'dept'));
$mediaChannelCode = MediaChannelService::normalizeStatsCode((string) ($params['media_channel_code'] ?? ''));
$mediaChannel = $mediaChannelCode !== '' ? MediaChannelService::getChannelByCode($mediaChannelCode) : null;
@@ -156,7 +158,17 @@ class ConversionLogic
$visibleAdminIds,
$excludeCancelledAppointments
);
self::hydrateOrderAndAmountStats($entities, $dimension, $entityIds, $adminToDeptIds, $startTimestamp, $endTimestamp, $mediaChannel, $visibleAdminIds);
self::hydrateOrderAndAmountStats(
$entities,
$dimension,
$entityIds,
$adminToDeptIds,
$startTimestamp,
$endTimestamp,
$mediaChannel,
$visibleAdminIds,
$usePerformanceOrderMetrics
);
// 数据隔离:可见部门 = 可见 admin 所属部门并集;用于 account_cost 与下游 cost 分摊。
$visibleDeptIds = self::resolveVisibleDeptIds($visibleAdminIds);
[$globalAccountCost, $accountCostDeptIds] = self::hydrateAccountCostStats($entities, $startDate, $endDate, $mediaChannelCode, $visibleDeptIds);
@@ -713,8 +725,8 @@ class ConversionLogic
/**
* @return array<int, int[]>
*
* 返回 admin_id => [dept_id, ...],每个 admin 的 dept_id 列表按 dept_id 升序排列
* zyt_admin_dept 联合主键 admin_id+dept_id,无独立 id 列)
* 返回 admin_id => [dept_id, ...]。跨部门时优先最深层、再按部门排序,
* 与一诊挂号统计和业绩看板的人员归属规则保持一致
* 当 admin 跨部门时,下游 mapEntityIds 只取列表中第一个落在当前 entityIds 内的部门,
* 避免同一笔加粉/挂号/接诊被多次累加到不同部门。
*/
@@ -722,10 +734,38 @@ class ConversionLogic
{
$rows = Db::name('admin_dept')
->field('admin_id, dept_id')
->order('admin_id', 'asc')
->order('dept_id', 'asc')
->select()
->toArray();
$deptRows = Db::name('dept')
->whereNull('delete_time')
->field('id, pid, sort')
->select()
->toArray();
$deptById = [];
foreach ($deptRows as $deptRow) {
$deptId = (int)($deptRow['id'] ?? 0);
if ($deptId > 0) {
$deptById[$deptId] = [
'pid' => (int)($deptRow['pid'] ?? 0),
'sort' => (int)($deptRow['sort'] ?? 0),
];
}
}
$depthCache = [];
$depthOf = static function (int $deptId) use (&$depthOf, &$depthCache, $deptById): int {
if ($deptId <= 0 || !isset($deptById[$deptId])) {
return 0;
}
if (isset($depthCache[$deptId])) {
return $depthCache[$deptId];
}
$parentId = (int)($deptById[$deptId]['pid'] ?? 0);
if ($parentId <= 0 || $parentId === $deptId || !isset($deptById[$parentId])) {
return $depthCache[$deptId] = 0;
}
return $depthCache[$deptId] = $depthOf($parentId) + 1;
};
$map = [];
foreach ($rows as $row) {
$adminId = (int)($row['admin_id'] ?? 0);
@@ -736,6 +776,21 @@ class ConversionLogic
$map[$adminId] ??= [];
$map[$adminId][] = $deptId;
}
foreach ($map as &$deptIds) {
usort($deptIds, static function (int $left, int $right) use ($depthOf, $deptById): int {
$depthCompare = $depthOf($right) <=> $depthOf($left);
if ($depthCompare !== 0) {
return $depthCompare;
}
$sortCompare = (int)($deptById[$right]['sort'] ?? 0) <=> (int)($deptById[$left]['sort'] ?? 0);
if ($sortCompare !== 0) {
return $sortCompare;
}
return $left <=> $right;
});
}
unset($deptIds);
return $map;
}
@@ -930,7 +985,9 @@ class ConversionLogic
?array $visibleAdminIds = null,
bool $excludeCancelledAppointments = false
): void {
$sourceExpr = $dimension === 'doctor' ? 'a.doctor_id' : 'u.assistant_id';
$sourceExpr = $dimension === 'doctor'
? 'a.doctor_id'
: 'COALESCE(NULLIF(a.assistant_id, 0), NULLIF(u.assistant_id, 0))';
$query = Db::name('doctor_appointment')
->alias('a')
->leftJoin('tcm_diagnosis u', 'a.patient_id = u.id')
@@ -940,7 +997,7 @@ class ConversionLogic
->group("{$sourceExpr}, a.patient_id");
if ($excludeCancelledAppointments) {
$query->where('a.status', '<>', 2);
$query->whereIn('a.status', [1, 3, 4]);
}
$query->where(static function (Query $subQuery): void {
@@ -1059,8 +1116,59 @@ class ConversionLogic
int $startTimestamp,
int $endTimestamp,
?array $mediaChannel,
?array $visibleAdminIds = null
?array $visibleAdminIds = null,
bool $usePerformanceOrderMetrics = false
): void {
if ($usePerformanceOrderMetrics) {
$sourceExpr = $dimension === 'doctor' ? 'rx.creator_id' : 'po.creator_id';
$query = Db::name('tcm_prescription_order')
->alias('po')
->leftJoin('tcm_prescription rx', 'rx.id = po.prescription_id AND rx.delete_time IS NULL')
->leftJoin('tcm_diagnosis dg', 'dg.id = po.diagnosis_id AND dg.delete_time IS NULL')
->whereNull('po.delete_time')
->where('po.create_time', 'between', [$startTimestamp, $endTimestamp]);
YejiStatsLogic::applyPrescriptionOrderEffectiveAmountQuery($query, 'po');
$query
->fieldRaw("{$sourceExpr} AS source_admin_id, COUNT(*) AS order_count, SUM(po.amount) AS total_amount")
->group($sourceExpr);
if ($mediaChannel !== null) {
$query
->leftJoin('order o', 'o.id = po.linked_pay_order_id')
->leftJoin('qywx_external_contact q', 'q.external_userid = o.payer_external_userid');
MediaChannelService::applyFollowUsersChannelFilter($query, 'q.follow_users', $mediaChannel);
}
foreach ($query->select()->toArray() as $row) {
$sourceAdminId = (int)($row['source_admin_id'] ?? 0);
$mappedEntityIds = self::mapEntityIds(
$dimension,
$sourceAdminId,
$entityIds,
$adminToDeptIds,
$visibleAdminIds
);
if ($mappedEntityIds === []) {
continue;
}
$orderCount = (int)($row['order_count'] ?? 0);
$amount = round((float)($row['total_amount'] ?? 0), 2);
foreach ($mappedEntityIds as $entityId) {
$entities[$entityId]['completed_order_count'] += $orderCount;
$entities[$entityId]['completed_order_amount'] = round(
(float)$entities[$entityId]['completed_order_amount'] + $amount,
2
);
$entities[$entityId]['business_order_amount'] = round(
(float)$entities[$entityId]['business_order_amount'] + $amount,
2
);
}
}
return;
}
$completedSourceExpr = $dimension === 'doctor' ? 'rx.creator_id' : 'rx.assistant_id';
$completedQuery = Db::name('tcm_prescription_order')
->alias('po')