增加自媒体渠道

This commit is contained in:
2026-04-23 15:43:22 +08:00
parent 366b005d78
commit 54431cbeb1
19 changed files with 737 additions and 47 deletions
@@ -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>