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
@@ -75,6 +75,8 @@ class FirstVisitConversionLogic
'end_date' => $endDate,
'include_filters' => 0,
'include_members' => 0,
'exclude_cancelled_appointments' => 1,
'order_metric_mode' => 'performance',
'page_no' => 1,
'page_size' => 100,
];
@@ -138,6 +140,8 @@ class FirstVisitConversionLogic
'selected_dept_name' => $selectedDeptName,
'selected_assistant_name' => $selectedAssistantName,
'open_count_source' => '个人业绩录入',
'appointment_rule' => '按预约日期统计,归属优先挂号医助、再回退诊单医助;仅含已预约、已完成和已过号',
'performance_rule' => '按业务订单创建时间和创建人统计,排除取消、拒收、退款及已发生退款的订单',
],
'filters' => [
'departments' => DeptLogic::getAllDataScoped($adminId, $adminInfo),
@@ -318,6 +322,51 @@ class FirstVisitConversionLogic
foreach ($deptRows as $deptRow) {
$adminDeptMap[(int) $deptRow['admin_id']][] = (int) $deptRow['dept_id'];
}
$deptMetaRows = Db::name('dept')
->whereNull('delete_time')
->field('id, pid, sort')
->select()
->toArray();
$deptMeta = [];
foreach ($deptMetaRows as $deptMetaRow) {
$deptId = (int) ($deptMetaRow['id'] ?? 0);
if ($deptId > 0) {
$deptMeta[$deptId] = [
'pid' => (int) ($deptMetaRow['pid'] ?? 0),
'sort' => (int) ($deptMetaRow['sort'] ?? 0),
];
}
}
$depthCache = [];
$depthOf = static function (int $deptId) use (&$depthOf, &$depthCache, $deptMeta): int {
if ($deptId <= 0 || !isset($deptMeta[$deptId])) {
return 0;
}
if (isset($depthCache[$deptId])) {
return $depthCache[$deptId];
}
$parentId = (int) ($deptMeta[$deptId]['pid'] ?? 0);
if ($parentId <= 0 || $parentId === $deptId || !isset($deptMeta[$parentId])) {
return $depthCache[$deptId] = 0;
}
return $depthCache[$deptId] = $depthOf($parentId) + 1;
};
foreach ($adminDeptMap as &$deptIds) {
usort($deptIds, static function (int $left, int $right) use ($depthOf, $deptMeta): int {
$depthCompare = $depthOf($right) <=> $depthOf($left);
if ($depthCompare !== 0) {
return $depthCompare;
}
$sortCompare = (int) ($deptMeta[$right]['sort'] ?? 0) <=> (int) ($deptMeta[$left]['sort'] ?? 0);
if ($sortCompare !== 0) {
return $sortCompare;
}
return $left <=> $right;
});
}
unset($deptIds);
$direct = [];
foreach ($rows as $row) {
@@ -449,17 +498,14 @@ class FirstVisitConversionLogic
if ($effectiveAdminIds !== []) {
$actualQuery = Db::name('tcm_prescription_order')
->alias('po')
->leftJoin('tcm_prescription rx', 'rx.id = po.prescription_id')
->whereNull('po.delete_time')
->where('po.prescription_audit_status', 1)
->where('po.payment_slip_audit_status', 1)
->where('po.create_time', 'between', [
strtotime($year . '-01-01 00:00:00'),
strtotime($year . '-12-31 23:59:59'),
]);
YejiStatsLogic::applyPrescriptionOrderEffectiveAmountQuery($actualQuery, 'po');
if ($effectiveAdminIds !== null) {
$actualQuery->whereIn('rx.assistant_id', $effectiveAdminIds);
$actualQuery->whereIn('po.creator_id', $effectiveAdminIds);
}
$actualRows = $actualQuery
->fieldRaw("DATE_FORMAT(FROM_UNIXTIME(po.create_time), '%m') AS month_no, SUM(po.amount) AS actual_amount")
@@ -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')