1
This commit is contained in:
@@ -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
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user