增加自媒体渠道
This commit is contained in:
@@ -6,6 +6,7 @@ namespace app\adminapi\logic\stats;
|
||||
|
||||
use app\adminapi\logic\dept\DeptLogic;
|
||||
use app\adminapi\logic\tcm\DiagnosisLogic;
|
||||
use app\common\service\qywx\MediaChannelService;
|
||||
use think\facade\Db;
|
||||
|
||||
class ConversionLogic
|
||||
@@ -20,6 +21,9 @@ class ConversionLogic
|
||||
{
|
||||
$includeFilters = (int)($params['include_filters'] ?? 0) === 1;
|
||||
$dimension = self::normalizeDimension((string)($params['dimension'] ?? 'dept'));
|
||||
$mediaChannelCode = MediaChannelService::normalizeStatsCode((string) ($params['media_channel_code'] ?? ''));
|
||||
$mediaChannel = $mediaChannelCode !== '' ? MediaChannelService::getChannelByCode($mediaChannelCode) : null;
|
||||
$filterEmptyEntities = $mediaChannel !== null;
|
||||
[$startTimestamp, $endTimestamp, $startDate, $endDate] = self::resolveTimeRange($params);
|
||||
$pageNo = max(1, (int)($params['page_no'] ?? 1));
|
||||
$pageSize = max(1, min(100, (int)($params['page_size'] ?? 15)));
|
||||
@@ -52,23 +56,24 @@ class ConversionLogic
|
||||
}
|
||||
|
||||
$adminToDeptIds = self::loadAdminDeptMap();
|
||||
self::hydrateFanStats($entities, $dimension, $entityIds, $adminToDeptIds, $startTimestamp, $endTimestamp);
|
||||
self::hydrateAppointmentStats($entities, $dimension, $entityIds, $adminToDeptIds, $startDate, $endDate);
|
||||
self::hydrateOrderAndAmountStats($entities, $dimension, $entityIds, $adminToDeptIds, $startTimestamp, $endTimestamp);
|
||||
self::hydrateAccountCostStats($entities, $startDate, $endDate);
|
||||
self::hydrateFanStats($entities, $dimension, $entityIds, $adminToDeptIds, $startTimestamp, $endTimestamp, $mediaChannel);
|
||||
self::hydrateAppointmentStats($entities, $dimension, $entityIds, $adminToDeptIds, $startDate, $endDate, $mediaChannel);
|
||||
self::hydrateOrderAndAmountStats($entities, $dimension, $entityIds, $adminToDeptIds, $startTimestamp, $endTimestamp, $mediaChannel);
|
||||
$globalAccountCost = self::hydrateAccountCostStats($entities, $startDate, $endDate, $mediaChannelCode);
|
||||
|
||||
if ($dimension === 'dept') {
|
||||
[$allRows, $pagedRows, $chartRows] = self::buildDeptTreeRows(
|
||||
$entities,
|
||||
isset($params['dept_id']) ? (int)$params['dept_id'] : 0,
|
||||
$pageNo,
|
||||
$pageSize
|
||||
$pageSize,
|
||||
$filterEmptyEntities
|
||||
);
|
||||
|
||||
$result = [
|
||||
'dimension' => $dimension,
|
||||
'date_range' => [$startDate, $endDate],
|
||||
'summary' => self::buildSummary($allRows),
|
||||
'summary' => self::buildSummary($allRows, $globalAccountCost),
|
||||
'charts' => self::buildCharts($chartRows),
|
||||
'lists' => $pagedRows,
|
||||
'count' => count($allRows),
|
||||
@@ -77,7 +82,7 @@ class ConversionLogic
|
||||
'extend' => [
|
||||
'dimension' => $dimension,
|
||||
'date_range' => [$startDate, $endDate],
|
||||
'summary' => self::buildSummary($allRows),
|
||||
'summary' => self::buildSummary($allRows, $globalAccountCost),
|
||||
'charts' => self::buildCharts($chartRows),
|
||||
],
|
||||
];
|
||||
@@ -88,14 +93,14 @@ class ConversionLogic
|
||||
return $result;
|
||||
}
|
||||
|
||||
$rows = self::finalizeRows($entities);
|
||||
$rows = self::finalizeRows($entities, $filterEmptyEntities);
|
||||
$count = count($rows);
|
||||
$offset = ($pageNo - 1) * $pageSize;
|
||||
|
||||
$result = [
|
||||
'dimension' => $dimension,
|
||||
'date_range' => [$startDate, $endDate],
|
||||
'summary' => self::buildSummary($rows),
|
||||
'summary' => self::buildSummary($rows, $globalAccountCost),
|
||||
'charts' => self::buildCharts($rows),
|
||||
'lists' => array_slice($rows, $offset, $pageSize),
|
||||
'count' => $count,
|
||||
@@ -104,7 +109,7 @@ class ConversionLogic
|
||||
'extend' => [
|
||||
'dimension' => $dimension,
|
||||
'date_range' => [$startDate, $endDate],
|
||||
'summary' => self::buildSummary($rows),
|
||||
'summary' => self::buildSummary($rows, $globalAccountCost),
|
||||
'charts' => self::buildCharts($rows),
|
||||
],
|
||||
];
|
||||
@@ -124,6 +129,7 @@ class ConversionLogic
|
||||
'departments' => DeptLogic::getAllData(),
|
||||
'assistants' => DiagnosisLogic::getAssistants(),
|
||||
'doctors' => DiagnosisLogic::getDoctors(),
|
||||
'media_channels' => MediaChannelService::getOptions(),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -351,6 +357,7 @@ class ConversionLogic
|
||||
* @param array<int, array<string, mixed>> $entities
|
||||
* @param int[] $entityIds
|
||||
* @param array<int, int[]> $adminToDeptIds
|
||||
* @param array<string, mixed>|null $mediaChannel
|
||||
*/
|
||||
private static function hydrateFanStats(
|
||||
array &$entities,
|
||||
@@ -358,17 +365,23 @@ class ConversionLogic
|
||||
array $entityIds,
|
||||
array $adminToDeptIds,
|
||||
int $startTimestamp,
|
||||
int $endTimestamp
|
||||
int $endTimestamp,
|
||||
?array $mediaChannel
|
||||
): void {
|
||||
$rows = Db::name('qywx_external_contact_event')
|
||||
$query = Db::name('qywx_external_contact_event')
|
||||
->alias('e')
|
||||
->leftJoin('admin a', 'a.work_wechat_userid = e.user_id AND a.delete_time IS NULL')
|
||||
->where('e.change_type', 'add_external_contact')
|
||||
->where('e.event_time', 'between', [$startTimestamp, $endTimestamp])
|
||||
->fieldRaw('e.user_id, a.id AS admin_id, COUNT(*) AS add_fans_count')
|
||||
->group('e.user_id, a.id')
|
||||
->select()
|
||||
->toArray();
|
||||
->group('e.user_id, a.id');
|
||||
|
||||
if ($mediaChannel !== null) {
|
||||
$query->leftJoin('qywx_external_contact q', 'q.external_userid = e.external_userid');
|
||||
MediaChannelService::applyFollowUsersChannelFilter($query, 'q.follow_users', $mediaChannel);
|
||||
}
|
||||
|
||||
$rows = $query->select()->toArray();
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$adminId = (int)($row['admin_id'] ?? 0);
|
||||
@@ -407,6 +420,7 @@ class ConversionLogic
|
||||
* @param array<int, array<string, mixed>> $entities
|
||||
* @param int[] $entityIds
|
||||
* @param array<int, int[]> $adminToDeptIds
|
||||
* @param array<string, mixed>|null $mediaChannel
|
||||
*/
|
||||
private static function hydrateAppointmentStats(
|
||||
array &$entities,
|
||||
@@ -414,19 +428,25 @@ class ConversionLogic
|
||||
array $entityIds,
|
||||
array $adminToDeptIds,
|
||||
string $startDate,
|
||||
string $endDate
|
||||
string $endDate,
|
||||
?array $mediaChannel
|
||||
): void {
|
||||
$sourceExpr = $dimension === 'doctor' ? 'a.doctor_id' : 'u.assistant_id';
|
||||
$rows = Db::name('doctor_appointment')
|
||||
$query = Db::name('doctor_appointment')
|
||||
->alias('a')
|
||||
->leftJoin('tcm_diagnosis u', 'a.patient_id = u.id')
|
||||
->where('a.appointment_date', '>=', $startDate)
|
||||
->where('a.appointment_date', '<=', $endDate)
|
||||
->whereRaw('(u.id IS NULL OR u.delete_time IS NULL)')
|
||||
->fieldRaw("{$sourceExpr} AS source_admin_id, a.patient_id AS diagnosis_id, COUNT(*) AS appointment_count, SUM(CASE WHEN a.status = 3 THEN 1 ELSE 0 END) AS interview_count")
|
||||
->group("{$sourceExpr}, a.patient_id")
|
||||
->select()
|
||||
->toArray();
|
||||
->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);
|
||||
}
|
||||
|
||||
$rows = $query->select()->toArray();
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$diagnosisId = (int)($row['diagnosis_id'] ?? 0);
|
||||
@@ -458,6 +478,7 @@ class ConversionLogic
|
||||
* @param array<int, array<string, mixed>> $entities
|
||||
* @param int[] $entityIds
|
||||
* @param array<int, int[]> $adminToDeptIds
|
||||
* @param array<string, mixed>|null $mediaChannel
|
||||
*/
|
||||
private static function hydrateOrderAndAmountStats(
|
||||
array &$entities,
|
||||
@@ -465,19 +486,26 @@ class ConversionLogic
|
||||
array $entityIds,
|
||||
array $adminToDeptIds,
|
||||
int $startTimestamp,
|
||||
int $endTimestamp
|
||||
int $endTimestamp,
|
||||
?array $mediaChannel
|
||||
): void {
|
||||
$sourceExpr = $dimension === 'doctor' ? 'rx.creator_id' : 'rx.assistant_id';
|
||||
$rows = Db::name('tcm_prescription_order')
|
||||
$query = Db::name('tcm_prescription_order')
|
||||
->alias('po')
|
||||
->leftJoin('tcm_prescription rx', 'rx.id = po.prescription_id')
|
||||
->leftJoin('tcm_diagnosis dg', 'dg.id = po.diagnosis_id')
|
||||
->whereNull('po.delete_time')
|
||||
->where('po.payment_slip_audit_status', 1)
|
||||
->where('po.create_time', 'between', [$startTimestamp, $endTimestamp])
|
||||
->fieldRaw("{$sourceExpr} AS source_admin_id, SUM(CASE WHEN po.prescription_audit_status = 1 THEN 1 ELSE 0 END) AS order_count, SUM(CASE WHEN po.prescription_audit_status = 1 THEN po.amount ELSE 0 END) AS total_amount")
|
||||
->group($sourceExpr)
|
||||
->select()
|
||||
->toArray();
|
||||
->group($sourceExpr);
|
||||
|
||||
if ($mediaChannel !== null) {
|
||||
$query->leftJoin('qywx_external_contact q', 'q.external_userid = dg.external_userid');
|
||||
MediaChannelService::applyFollowUsersChannelFilter($query, 'q.follow_users', $mediaChannel);
|
||||
}
|
||||
|
||||
$rows = $query->select()->toArray();
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$sourceAdminId = (int)($row['source_admin_id'] ?? 0);
|
||||
@@ -499,22 +527,29 @@ class ConversionLogic
|
||||
|
||||
/**
|
||||
* 账户消耗:来源于独立维护表 zyt_account_cost,为全局日维度数据。
|
||||
* 由于当前没有部门/医助/医生拆分来源,只在汇总层使用,不向各明细行分摊。
|
||||
* 由于当前没有部门/医助/医生拆分来源,只在当前筛选口径的汇总层使用,不向各明细行分摊。
|
||||
*
|
||||
* @param array<int, array<string, mixed>> $entities
|
||||
*/
|
||||
private static function hydrateAccountCostStats(array &$entities, string $startDate, string $endDate): void
|
||||
private static function hydrateAccountCostStats(array &$entities, string $startDate, string $endDate, string $mediaChannelCode): float
|
||||
{
|
||||
$totalAmount = round((float) Db::name('account_cost')
|
||||
$query = Db::name('account_cost')
|
||||
->where('cost_date', '>=', $startDate)
|
||||
->where('cost_date', '<=', $endDate)
|
||||
->sum('amount'), 2);
|
||||
->where('cost_date', '<=', $endDate);
|
||||
|
||||
if ($mediaChannelCode !== '') {
|
||||
$query->where('media_channel_code', $mediaChannelCode);
|
||||
}
|
||||
|
||||
$totalAmount = round((float) $query->sum('amount'), 2);
|
||||
|
||||
foreach ($entities as &$entity) {
|
||||
$entity['account_cost'] = 0.0;
|
||||
$entity['_global_account_cost'] = $totalAmount;
|
||||
}
|
||||
unset($entity);
|
||||
|
||||
return $totalAmount;
|
||||
}
|
||||
|
||||
|
||||
@@ -546,7 +581,7 @@ class ConversionLogic
|
||||
* @param array<int, array<string, mixed>> $entities
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
private static function finalizeRows(array $entities): array
|
||||
private static function finalizeRows(array $entities, bool $excludeEmpty = false): array
|
||||
{
|
||||
$rows = [];
|
||||
$globalAccountCost = 0.0;
|
||||
@@ -578,6 +613,10 @@ class ConversionLogic
|
||||
$entity['cash_cost'] = self::safeDivideMoney($effectiveAccountCost, $addFansCount);
|
||||
$entity['roi'] = self::safeDivideRatio($completedOrderAmount, $effectiveAccountCost);
|
||||
|
||||
if ($excludeEmpty && !self::hasBusinessMetrics($entity)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$rows[] = $entity;
|
||||
}
|
||||
|
||||
@@ -608,7 +647,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): array
|
||||
private static function buildDeptTreeRows(array $entities, int $selectedDeptId, int $pageNo, int $pageSize, bool $excludeEmpty = false): array
|
||||
{
|
||||
$childrenByPid = [];
|
||||
foreach ($entities as $entity) {
|
||||
@@ -668,6 +707,12 @@ class ConversionLogic
|
||||
});
|
||||
|
||||
$rootRows = array_map(static fn (int $deptId): array => self::finalizeDeptNode($buildNode($deptId)), $rootIds);
|
||||
if ($excludeEmpty) {
|
||||
$rootRows = array_values(array_filter(array_map(
|
||||
static fn (array $row): ?array => self::pruneDeptNode($row),
|
||||
$rootRows
|
||||
)));
|
||||
}
|
||||
$realRootRows = array_values(array_filter($rootRows, static fn (array $row): bool => !((bool)($row['_virtual_bucket'] ?? false))));
|
||||
$virtualRoot = count($realRootRows) === 1 && !empty($realRootRows[0]['children']) ? $realRootRows[0] : null;
|
||||
|
||||
@@ -713,6 +758,31 @@ class ConversionLogic
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $node
|
||||
* @return array<string, mixed>|null
|
||||
*/
|
||||
private static function pruneDeptNode(array $node): ?array
|
||||
{
|
||||
$children = [];
|
||||
foreach ($node['children'] ?? [] as $child) {
|
||||
if (!is_array($child)) {
|
||||
continue;
|
||||
}
|
||||
$pruned = self::pruneDeptNode($child);
|
||||
if ($pruned !== null) {
|
||||
$children[] = $pruned;
|
||||
}
|
||||
}
|
||||
$node['children'] = $children;
|
||||
|
||||
if ($children !== []) {
|
||||
return $node;
|
||||
}
|
||||
|
||||
return self::hasBusinessMetrics($node) ? $node : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $node
|
||||
* @return array<string, mixed>
|
||||
@@ -758,7 +828,7 @@ class ConversionLogic
|
||||
* @param array<int, array<string, mixed>> $rows
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private static function buildSummary(array $rows): array
|
||||
private static function buildSummary(array $rows, float $forcedAccountCost = 0.0): array
|
||||
{
|
||||
$summary = [
|
||||
'add_fans_count' => 0,
|
||||
@@ -787,7 +857,7 @@ class ConversionLogic
|
||||
$globalAccountCost = max($globalAccountCost, round((float)($row['_global_account_cost'] ?? 0), 2));
|
||||
}
|
||||
|
||||
$summary['account_cost'] = $globalAccountCost;
|
||||
$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']);
|
||||
@@ -802,6 +872,20 @@ class ConversionLogic
|
||||
return $summary;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $row
|
||||
*/
|
||||
private static function hasBusinessMetrics(array $row): bool
|
||||
{
|
||||
return (int)($row['add_fans_count'] ?? 0) > 0
|
||||
|| (int)($row['paid_appointment_count'] ?? 0) > 0
|
||||
|| (int)($row['free_appointment_count'] ?? 0) > 0
|
||||
|| (int)($row['appointment_total_count'] ?? 0) > 0
|
||||
|| (int)($row['interview_count'] ?? 0) > 0
|
||||
|| (int)($row['completed_order_count'] ?? 0) > 0
|
||||
|| (float)($row['completed_order_amount'] ?? 0) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array<string, mixed>> $rows
|
||||
* @return array<string, mixed>
|
||||
|
||||
Reference in New Issue
Block a user