This commit is contained in:
2026-04-24 17:17:31 +08:00
parent d408ffabe8
commit 5cb9706274
2 changed files with 210 additions and 51 deletions
@@ -28,8 +28,11 @@ class ConversionLogic
$pageNo = max(1, (int)($params['page_no'] ?? 1));
$pageSize = max(1, min(100, (int)($params['page_size'] ?? 15)));
$allocationParams = self::buildAllocationScopeParams($dimension, $params);
$entities = self::loadEntities($dimension, $params);
$entityIds = array_keys($entities);
$allocationEntities = self::loadEntities($dimension, $allocationParams);
$allocationEntityIds = array_keys($allocationEntities);
if ($entityIds === []) {
$result = [
@@ -56,8 +59,10 @@ class ConversionLogic
}
$adminToDeptIds = self::loadAdminDeptMap();
self::hydrateFanStats($allocationEntities, $dimension, $allocationEntityIds, $adminToDeptIds, $startTimestamp, $endTimestamp, $mediaChannel);
$allocationAddFansCount = self::sumEntityAddFans(array_values($allocationEntities));
self::hydrateFanStats($entities, $dimension, $entityIds, $adminToDeptIds, $startTimestamp, $endTimestamp, $mediaChannel);
self::hydrateAppointmentStats($entities, $dimension, $entityIds, $adminToDeptIds, $startDate, $endDate, $mediaChannel);
self::hydrateAppointmentStats($entities, $dimension, $entityIds, $adminToDeptIds, $startTimestamp, $endTimestamp, $startDate, $endDate, $mediaChannel);
self::hydrateOrderAndAmountStats($entities, $dimension, $entityIds, $adminToDeptIds, $startTimestamp, $endTimestamp, $mediaChannel);
$globalAccountCost = self::hydrateAccountCostStats($entities, $startDate, $endDate, $mediaChannelCode);
@@ -67,13 +72,14 @@ class ConversionLogic
isset($params['dept_id']) ? (int)$params['dept_id'] : 0,
$pageNo,
$pageSize,
$filterEmptyEntities
$filterEmptyEntities,
$allocationAddFansCount
);
$result = [
'dimension' => $dimension,
'date_range' => [$startDate, $endDate],
'summary' => self::buildSummary($allRows, $globalAccountCost),
'summary' => self::buildSummary($allRows),
'charts' => self::buildCharts($chartRows),
'lists' => $pagedRows,
'count' => count($allRows),
@@ -82,7 +88,7 @@ class ConversionLogic
'extend' => [
'dimension' => $dimension,
'date_range' => [$startDate, $endDate],
'summary' => self::buildSummary($allRows, $globalAccountCost),
'summary' => self::buildSummary($allRows),
'charts' => self::buildCharts($chartRows),
],
];
@@ -93,14 +99,14 @@ class ConversionLogic
return $result;
}
$rows = self::finalizeRows($entities, $filterEmptyEntities);
$rows = self::finalizeRows($entities, $filterEmptyEntities, $allocationAddFansCount);
$count = count($rows);
$offset = ($pageNo - 1) * $pageSize;
$result = [
'dimension' => $dimension,
'date_range' => [$startDate, $endDate],
'summary' => self::buildSummary($rows, $globalAccountCost),
'summary' => self::buildSummary($rows),
'charts' => self::buildCharts($rows),
'lists' => array_slice($rows, $offset, $pageSize),
'count' => $count,
@@ -109,7 +115,7 @@ class ConversionLogic
'extend' => [
'dimension' => $dimension,
'date_range' => [$startDate, $endDate],
'summary' => self::buildSummary($rows, $globalAccountCost),
'summary' => self::buildSummary($rows),
'charts' => self::buildCharts($rows),
],
];
@@ -120,6 +126,15 @@ class ConversionLogic
return $result;
}
private static function buildAllocationScopeParams(string $dimension, array $params): array
{
$allocationParams = $params;
unset($allocationParams['dept_id'], $allocationParams['assistant_id'], $allocationParams['doctor_id']);
return $allocationParams;
}
/**
* @return array<string, mixed>
*/
@@ -432,6 +447,8 @@ class ConversionLogic
string $dimension,
array $entityIds,
array $adminToDeptIds,
int $startTimestamp,
int $endTimestamp,
string $startDate,
string $endDate,
?array $mediaChannel
@@ -447,8 +464,13 @@ class ConversionLogic
->group("{$sourceExpr}, a.patient_id");
if ($mediaChannel !== null) {
$query->leftJoin('qywx_external_contact q', 'q.external_userid = u.external_userid');
MediaChannelService::applyFollowUsersChannelFilter($query, 'q.follow_users', $mediaChannel);
$legacyChannelValues = MediaChannelService::getLegacyAppointmentChannelValues($mediaChannel);
if ($legacyChannelValues !== []) {
$query->whereIn('a.channels', $legacyChannelValues);
} else {
$query->leftJoin('qywx_external_contact q', 'q.external_userid = u.external_userid');
MediaChannelService::applyFollowUsersChannelFilter($query, 'q.follow_users', $mediaChannel);
}
}
$rows = $query->select()->toArray();
@@ -470,13 +492,65 @@ class ConversionLogic
$interviewCount = (int)($row['interview_count'] ?? 0);
foreach ($mappedEntityIds as $entityId) {
$entities[$entityId]['appointment_total_count'] += $appointmentCount;
$entities[$entityId]['paid_appointment_count'] += 1;
if ($appointmentCount > 1) {
$entities[$entityId]['free_appointment_count'] += ($appointmentCount - 1);
}
$entities[$entityId]['interview_count'] += $interviewCount;
}
}
self::hydratePaidAppointmentStats($entities, $dimension, $entityIds, $adminToDeptIds, $startTimestamp, $endTimestamp, $mediaChannel);
foreach ($entities as &$entity) {
$appointmentTotalCount = (int)($entity['appointment_total_count'] ?? 0);
$paidAppointmentCount = (int)($entity['paid_appointment_count'] ?? 0);
$entity['free_appointment_count'] = max($appointmentTotalCount - $paidAppointmentCount, 0);
}
unset($entity);
}
/**
* @param array<int, array<string, mixed>> $entities
* @param int[] $entityIds
* @param array<int, int[]> $adminToDeptIds
* @param array<string, mixed>|null $mediaChannel
*/
private static function hydratePaidAppointmentStats(
array &$entities,
string $dimension,
array $entityIds,
array $adminToDeptIds,
int $startTimestamp,
int $endTimestamp,
?array $mediaChannel
): void {
$startDateTime = date('Y-m-d H:i:s', $startTimestamp);
$endDateTime = date('Y-m-d H:i:s', $endTimestamp);
$query = Db::name('order')
->alias('o')
->whereNull('o.delete_time')
->where('o.status', 2)
->where('o.order_type', 1)
->where('o.amount', 5)
->whereRaw('(o.payment_time IS NOT NULL AND o.payment_time <> "" AND o.payment_time >= ? AND o.payment_time <= ?)', [$startDateTime, $endDateTime])
->fieldRaw('o.creator_id AS source_admin_id, COUNT(*) AS paid_appointment_count')
->group('o.creator_id');
if ($mediaChannel !== null) {
$query->leftJoin('qywx_external_contact q', 'q.external_userid = o.payer_external_userid');
MediaChannelService::applyFollowUsersChannelFilter($query, 'q.follow_users', $mediaChannel);
}
$rows = $query->select()->toArray();
foreach ($rows as $row) {
$sourceAdminId = (int)($row['source_admin_id'] ?? 0);
$mappedEntityIds = self::mapEntityIds($dimension, $sourceAdminId, $entityIds, $adminToDeptIds);
if ($mappedEntityIds === []) {
continue;
}
$count = (int)($row['paid_appointment_count'] ?? 0);
foreach ($mappedEntityIds as $entityId) {
$entities[$entityId]['paid_appointment_count'] += $count;
}
}
}
/**
@@ -506,7 +580,9 @@ class ConversionLogic
->group($completedSourceExpr);
if ($mediaChannel !== null) {
$completedQuery->leftJoin('qywx_external_contact q', 'q.external_userid = dg.external_userid');
$completedQuery
->leftJoin('order o', 'o.id = po.linked_pay_order_id')
->leftJoin('qywx_external_contact q', 'q.external_userid = o.payer_external_userid');
MediaChannelService::applyFollowUsersChannelFilter($completedQuery, 'q.follow_users', $mediaChannel);
}
@@ -540,7 +616,9 @@ class ConversionLogic
->group($businessSourceExpr);
if ($mediaChannel !== null) {
$businessQuery->leftJoin('qywx_external_contact q2', 'q2.external_userid = dg.external_userid');
$businessQuery
->leftJoin('order o2', 'o2.id = po.linked_pay_order_id')
->leftJoin('qywx_external_contact q2', 'q2.external_userid = o2.payer_external_userid');
MediaChannelService::applyFollowUsersChannelFilter($businessQuery, 'q2.follow_users', $mediaChannel);
}
@@ -616,11 +694,17 @@ class ConversionLogic
* @param array<int, array<string, mixed>> $entities
* @return array<int, array<string, mixed>>
*/
private static function finalizeRows(array $entities, bool $excludeEmpty = false): array
private static function finalizeRows(array $entities, bool $excludeEmpty = false, ?int $allocationAddFansCount = null): array
{
$rows = [];
$globalAccountCost = 0.0;
foreach ($entities as $entity) {
$filteredEntities = $excludeEmpty
? array_values(array_filter($entities, static fn (array $entity): bool => self::hasBusinessMetrics($entity)))
: array_values($entities);
$globalAccountCost = self::extractGlobalAccountCost($filteredEntities);
$globalAddFansCount = $allocationAddFansCount ?? self::sumEntityAddFans($filteredEntities);
foreach ($filteredEntities as $entity) {
$paidAppointmentCount = (int)($entity['paid_appointment_count'] ?? 0);
$freeAppointmentCount = (int)($entity['free_appointment_count'] ?? 0);
$appointmentTotalCount = (int)($entity['appointment_total_count'] ?? 0);
@@ -630,9 +714,7 @@ class ConversionLogic
$completedOrderCount = (int)$entity['completed_order_count'];
$businessOrderAmount = round((float)$entity['business_order_amount'], 2);
$completedOrderAmount = round((float)$entity['completed_order_amount'], 2);
$accountCost = round((float)$entity['account_cost'], 2);
$globalAccountCost = max($globalAccountCost, round((float)($entity['_global_account_cost'] ?? 0), 2));
$effectiveAccountCost = $globalAccountCost > 0 ? $globalAccountCost : $accountCost;
$effectiveAccountCost = self::allocateAccountCost($globalAccountCost, $globalAddFansCount, $addFansCount);
$entity['paid_appointment_count'] = $paidAppointmentCount;
$entity['free_appointment_count'] = $freeAppointmentCount;
@@ -649,9 +731,8 @@ class ConversionLogic
$entity['avg_unit_price'] = self::safeDivideMoney($completedOrderAmount, $completedOrderCount);
$entity['cash_cost'] = self::safeDivideMoney($effectiveAccountCost, $addFansCount);
$entity['roi'] = self::safeDivideRatio($completedOrderAmount, $effectiveAccountCost);
if ($excludeEmpty && !self::hasBusinessMetrics($entity)) {
continue;
if ($globalAccountCost > 0) {
$entity['_global_account_cost'] = $globalAccountCost;
}
$rows[] = $entity;
@@ -670,13 +751,6 @@ class ConversionLogic
return $right['add_fans_count'] <=> $left['add_fans_count'];
});
if ($globalAccountCost > 0 && $rows !== []) {
foreach ($rows as &$row) {
$row['_global_account_cost'] = $globalAccountCost;
}
unset($row);
}
return $rows;
}
@@ -684,7 +758,7 @@ class ConversionLogic
* @param array<int, array<string, mixed>> $entities
* @return array{0: array<int, array<string, mixed>>, 1: array<int, array<string, mixed>>, 2: array<int, array<string, mixed>>}
*/
private static function buildDeptTreeRows(array $entities, int $selectedDeptId, int $pageNo, int $pageSize, bool $excludeEmpty = false): array
private static function buildDeptTreeRows(array $entities, int $selectedDeptId, int $pageNo, int $pageSize, bool $excludeEmpty = false, ?int $allocationAddFansCount = null): array
{
$childrenByPid = [];
foreach ($entities as $entity) {
@@ -744,7 +818,7 @@ class ConversionLogic
return $left <=> $right;
});
$rootRows = array_map(static fn (int $deptId): array => self::finalizeDeptNode($buildNode($deptId)), $rootIds);
$rootRows = array_map($buildNode, $rootIds);
if ($excludeEmpty) {
$rootRows = array_values(array_filter(array_map(
static fn (array $row): ?array => self::pruneDeptNode($row),
@@ -757,17 +831,28 @@ class ConversionLogic
if ($selectedDeptId > 0) {
$selectedNode = self::findDeptNode($rootRows, $selectedDeptId);
if ($selectedNode !== null) {
$allRows = [$selectedNode];
$chartRows = !empty($selectedNode['children']) ? $selectedNode['children'] : [$selectedNode];
$allRowsRaw = [$selectedNode];
$chartRowsRaw = !empty($selectedNode['children']) ? $selectedNode['children'] : [$selectedNode];
} else {
$allRows = $rootRows;
$chartRows = $virtualRoot !== null ? ($virtualRoot['children'] ?? [$virtualRoot]) : ($realRootRows ?: $rootRows);
$allRowsRaw = $rootRows;
$chartRowsRaw = $virtualRoot !== null ? ($virtualRoot['children'] ?? [$virtualRoot]) : ($realRootRows ?: $rootRows);
}
} else {
$allRows = $rootRows;
$chartRows = $virtualRoot !== null ? ($virtualRoot['children'] ?? [$virtualRoot]) : ($realRootRows ?: $rootRows);
$allRowsRaw = $rootRows;
$chartRowsRaw = $virtualRoot !== null ? ($virtualRoot['children'] ?? [$virtualRoot]) : ($realRootRows ?: $rootRows);
}
$globalAccountCost = self::extractGlobalAccountCost($allRowsRaw);
$globalAddFansCount = $allocationAddFansCount ?? self::sumNodeAddFans($allRowsRaw);
$allRows = array_map(
static fn (array $row): array => self::finalizeDeptNode($row, $globalAccountCost, $globalAddFansCount),
$allRowsRaw
);
$chartRows = array_map(
static fn (array $row): array => self::finalizeDeptNode($row, $globalAccountCost, $globalAddFansCount),
$chartRowsRaw
);
$offset = ($pageNo - 1) * $pageSize;
return [$allRows, array_slice($allRows, $offset, $pageSize), $chartRows];
@@ -825,9 +910,12 @@ class ConversionLogic
* @param array<string, mixed> $node
* @return array<string, mixed>
*/
private static function finalizeDeptNode(array $node): array
private static function finalizeDeptNode(array $node, float $globalAccountCost, int $globalAddFansCount): array
{
$node['children'] = array_map(static fn (array $child): array => self::finalizeDeptNode($child), $node['children'] ?? []);
$node['children'] = array_map(
static fn (array $child): array => self::finalizeDeptNode($child, $globalAccountCost, $globalAddFansCount),
$node['children'] ?? []
);
$paidAppointmentCount = (int)($node['paid_appointment_count'] ?? 0);
$freeAppointmentCount = (int)($node['free_appointment_count'] ?? 0);
$appointmentTotalCount = (int)($node['appointment_total_count'] ?? 0);
@@ -837,9 +925,7 @@ class ConversionLogic
$completedOrderCount = (int)$node['completed_order_count'];
$businessOrderAmount = round((float)$node['business_order_amount'], 2);
$completedOrderAmount = round((float)$node['completed_order_amount'], 2);
$accountCost = round((float)$node['account_cost'], 2);
$globalAccountCost = round((float)($node['_global_account_cost'] ?? 0), 2);
$effectiveAccountCost = $globalAccountCost > 0 ? $globalAccountCost : $accountCost;
$effectiveAccountCost = self::allocateAccountCost($globalAccountCost, $globalAddFansCount, $addFansCount);
$node['paid_appointment_count'] = $paidAppointmentCount;
$node['free_appointment_count'] = $freeAppointmentCount;
@@ -868,7 +954,7 @@ class ConversionLogic
* @param array<int, array<string, mixed>> $rows
* @return array<string, mixed>
*/
private static function buildSummary(array $rows, float $forcedAccountCost = 0.0): array
private static function buildSummary(array $rows): array
{
$summary = [
'add_fans_count' => 0,
@@ -883,8 +969,6 @@ class ConversionLogic
'completed_order_count' => 0,
'account_cost' => 0.0,
];
$globalAccountCost = 0.0;
foreach ($rows as $row) {
$summary['add_fans_count'] += (int)$row['add_fans_count'];
$summary['total_open_count'] += (int)$row['total_open_count'];
@@ -896,11 +980,9 @@ class ConversionLogic
$summary['completed_order_count'] += (int)$row['completed_order_count'];
$summary['business_order_amount'] = round($summary['business_order_amount'] + (float)($row['business_order_amount'] ?? 0), 2);
$summary['completed_order_amount'] = round($summary['completed_order_amount'] + (float)$row['completed_order_amount'], 2);
$globalAccountCost = max($globalAccountCost, round((float)($row['_global_account_cost'] ?? 0), 2));
$summary['account_cost'] = round($summary['account_cost'] + (float)($row['account_cost'] ?? 0), 2);
}
$summary['account_cost'] = max($globalAccountCost, round($forcedAccountCost, 2));
$summary['paid_appointment_rate'] = self::percent($summary['paid_appointment_count'], $summary['add_fans_count']);
$summary['open_appointment_rate'] = self::percent($summary['paid_appointment_count'], $summary['total_open_count']);
$summary['interview_rate'] = self::percent($summary['interview_count'], $summary['appointment_total_count']);
@@ -914,6 +996,54 @@ class ConversionLogic
return $summary;
}
/**
* @param array<int, array<string, mixed>> $entities
*/
private static function extractGlobalAccountCost(array $entities): float
{
$globalAccountCost = 0.0;
foreach ($entities as $entity) {
$globalAccountCost = max($globalAccountCost, round((float)($entity['_global_account_cost'] ?? 0), 2));
}
return $globalAccountCost;
}
/**
* @param array<int, array<string, mixed>> $entities
*/
private static function sumEntityAddFans(array $entities): int
{
$total = 0;
foreach ($entities as $entity) {
$total += (int)($entity['add_fans_count'] ?? 0);
}
return $total;
}
/**
* @param array<int, array<string, mixed>> $nodes
*/
private static function sumNodeAddFans(array $nodes): int
{
$total = 0;
foreach ($nodes as $node) {
$total += (int)($node['add_fans_count'] ?? 0);
}
return $total;
}
private static function allocateAccountCost(float $globalAccountCost, int $globalAddFansCount, int $rowAddFansCount): float
{
if ($globalAccountCost <= 0 || $globalAddFansCount <= 0 || $rowAddFansCount <= 0) {
return 0.0;
}
return round(($globalAccountCost / $globalAddFansCount) * $rowAddFansCount, 2);
}
/**
* @param array<string, mixed> $row
*/