386 lines
14 KiB
PHP
386 lines
14 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace app\adminapi\logic\stats;
|
|
|
|
use app\common\logic\BaseLogic;
|
|
use app\common\model\dict\DictData;
|
|
use app\common\model\stats\PersonalAccountCost;
|
|
use app\common\model\stats\PersonalYeji;
|
|
|
|
class SelfInputLogic extends BaseLogic
|
|
{
|
|
use PersonalStatsScopeTrait;
|
|
|
|
public static function overview(array $params, int $adminId, array $adminInfo): array
|
|
{
|
|
[$startDate, $endDate] = self::resolveTimeRange($params);
|
|
$pageNo = max(1, (int) ($params['page_no'] ?? 1));
|
|
$pageSize = min(100, max(1, (int) ($params['page_size'] ?? 15)));
|
|
$mediaSource = trim((string) ($params['media_source'] ?? ''));
|
|
$deptId = (int) ($params['dept_id'] ?? 0);
|
|
|
|
$effectiveAdminIds = self::resolveEffectiveAdminIds($adminId, $adminInfo, $deptId);
|
|
|
|
$yejiQuery = self::buildYejiQuery($startDate, $endDate, $effectiveAdminIds);
|
|
if ($mediaSource !== '') {
|
|
$yejiQuery->where('media_source', $mediaSource);
|
|
}
|
|
|
|
$count = (int) (clone $yejiQuery)->count();
|
|
$rows = (clone $yejiQuery)
|
|
->order(['yeji_date' => 'desc', 'id' => 'desc'])
|
|
->page($pageNo, $pageSize)
|
|
->select()
|
|
->toArray();
|
|
|
|
$costMap = self::loadAccountCostMap($startDate, $endDate, $effectiveAdminIds, $mediaSource);
|
|
|
|
$lists = [];
|
|
foreach ($rows as $row) {
|
|
$entity = self::normalizeYejiRow($row);
|
|
$costKey = self::buildCostKey(
|
|
(int) $entity['creator_id'],
|
|
(string) $entity['yeji_date'],
|
|
(string) $entity['media_source']
|
|
);
|
|
$entity['account_cost'] = round((float) ($costMap[$costKey] ?? 0), 2);
|
|
$lists[] = self::finalizeMetrics($entity);
|
|
}
|
|
$lists = self::attachDeptInfoToRows($lists);
|
|
|
|
$allYejiRows = self::buildYejiQuery($startDate, $endDate, $effectiveAdminIds);
|
|
if ($mediaSource !== '') {
|
|
$allYejiRows->where('media_source', $mediaSource);
|
|
}
|
|
$allYeji = $allYejiRows->select()->toArray();
|
|
|
|
$summaryCostMap = $costMap;
|
|
$summaryBase = self::emptyMetrics();
|
|
foreach ($allYeji as $row) {
|
|
$entity = self::normalizeYejiRow($row);
|
|
$costKey = self::buildCostKey(
|
|
(int) $entity['creator_id'],
|
|
(string) $entity['yeji_date'],
|
|
(string) $entity['media_source']
|
|
);
|
|
$entity['account_cost'] = round((float) ($summaryCostMap[$costKey] ?? 0), 2);
|
|
unset($summaryCostMap[$costKey]);
|
|
$entity = self::finalizeMetrics($entity);
|
|
$summaryBase = self::accumulateMetrics($summaryBase, $entity);
|
|
}
|
|
|
|
foreach ($summaryCostMap as $amount) {
|
|
$summaryBase['account_cost'] += round((float) $amount, 2);
|
|
}
|
|
|
|
$summary = self::finalizeMetrics($summaryBase);
|
|
$canViewFinance = self::canViewAllSelfInputStats($adminInfo);
|
|
|
|
if (!$canViewFinance) {
|
|
$summary = self::maskFinanceFields($summary);
|
|
foreach ($lists as &$item) {
|
|
$item = self::maskFinanceFields($item);
|
|
}
|
|
unset($item);
|
|
}
|
|
|
|
return [
|
|
'summary' => $summary,
|
|
'lists' => $lists,
|
|
'count' => $count,
|
|
'page_no' => $pageNo,
|
|
'page_size' => $pageSize,
|
|
'extend' => [
|
|
'summary' => $summary,
|
|
'date_range' => [$startDate, $endDate],
|
|
'can_view_finance' => $canViewFinance,
|
|
],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* 财务相关字段(账户消耗 / 现金成本 / ROI):非豁免角色不可见,剔除字段避免被嗅探。
|
|
*
|
|
* @param array<string, mixed> $entity
|
|
* @return array<string, mixed>
|
|
*/
|
|
private static function maskFinanceFields(array $entity): array
|
|
{
|
|
foreach (['account_cost', 'cash_cost', 'roi'] as $key) {
|
|
unset($entity[$key]);
|
|
}
|
|
|
|
return $entity;
|
|
}
|
|
|
|
/**
|
|
* 自媒体来源选项:来自字典「推广渠道」(type_value=channels),按 sort/id 排序。
|
|
* 用 dict_data.name 作为存储值(与历史录入兼容;保留 value 仅作展示标识)。
|
|
*
|
|
* @return array<int, array{name: string, value: string}>
|
|
*/
|
|
public static function mediaSourceOptions(int $adminId, array $adminInfo): array
|
|
{
|
|
unset($adminId, $adminInfo);
|
|
|
|
$rows = DictData::where('type_value', 'channels')
|
|
->where('status', 1)
|
|
->order(['sort' => 'desc', 'id' => 'asc'])
|
|
->field('name, value')
|
|
->select()
|
|
->toArray();
|
|
|
|
$list = [];
|
|
$seen = [];
|
|
foreach ($rows as $row) {
|
|
$name = self::normalizeMediaSource((string) ($row['name'] ?? ''));
|
|
if ($name === '' || isset($seen[$name])) {
|
|
continue;
|
|
}
|
|
$seen[$name] = true;
|
|
$list[] = [
|
|
'name' => $name,
|
|
'value' => (string) ($row['value'] ?? ''),
|
|
];
|
|
}
|
|
|
|
return $list;
|
|
}
|
|
|
|
/**
|
|
* @return array<int>|null null = 不限;[] = 无可见
|
|
*/
|
|
private static function resolveEffectiveAdminIds(int $adminId, array $adminInfo, int $deptId): ?array
|
|
{
|
|
$visibleIds = self::getVisibleCreatorIds($adminId, $adminInfo);
|
|
|
|
return self::intersectVisibleByDept($visibleIds, $deptId);
|
|
}
|
|
|
|
/**
|
|
* @param array<int>|null $effectiveAdminIds
|
|
*/
|
|
private static function buildYejiQuery(string $startDate, string $endDate, ?array $effectiveAdminIds)
|
|
{
|
|
$query = PersonalYeji::whereBetween('yeji_date', [$startDate, $endDate]);
|
|
if ($effectiveAdminIds === []) {
|
|
$query->whereRaw('0 = 1');
|
|
} elseif ($effectiveAdminIds !== null) {
|
|
$query->whereIn('creator_id', $effectiveAdminIds);
|
|
}
|
|
|
|
return $query;
|
|
}
|
|
|
|
/**
|
|
* @param array<int>|null $effectiveAdminIds
|
|
* @return array<string, float>
|
|
*/
|
|
private static function loadAccountCostMap(
|
|
string $startDate,
|
|
string $endDate,
|
|
?array $effectiveAdminIds,
|
|
string $mediaSource
|
|
): array {
|
|
$query = PersonalAccountCost::whereBetween('cost_date', [$startDate, $endDate]);
|
|
if ($effectiveAdminIds === []) {
|
|
return [];
|
|
}
|
|
if ($effectiveAdminIds !== null) {
|
|
$query->whereIn('creator_id', $effectiveAdminIds);
|
|
}
|
|
if ($mediaSource !== '') {
|
|
$query->where('media_source', $mediaSource);
|
|
}
|
|
|
|
$rows = $query
|
|
->fieldRaw('creator_id, cost_date, media_source, SUM(amount) AS total_amount')
|
|
->group('creator_id, cost_date, media_source')
|
|
->select()
|
|
->toArray();
|
|
|
|
$map = [];
|
|
foreach ($rows as $row) {
|
|
$key = self::buildCostKey(
|
|
(int) $row['creator_id'],
|
|
(string) $row['cost_date'],
|
|
(string) $row['media_source']
|
|
);
|
|
$map[$key] = round((float) ($row['total_amount'] ?? 0), 2);
|
|
}
|
|
|
|
return $map;
|
|
}
|
|
|
|
private static function buildCostKey(int $creatorId, string $date, string $mediaSource): string
|
|
{
|
|
return $creatorId . '|' . $date . '|' . self::normalizeMediaSource($mediaSource);
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $row
|
|
* @return array<string, mixed>
|
|
*/
|
|
private static function normalizeYejiRow(array $row): array
|
|
{
|
|
return [
|
|
'id' => (int) ($row['id'] ?? 0),
|
|
'yeji_date' => (string) ($row['yeji_date'] ?? ''),
|
|
'media_source' => (string) ($row['media_source'] ?? ''),
|
|
'creator_id' => (int) ($row['creator_id'] ?? 0),
|
|
'creator_name' => (string) ($row['creator_name'] ?? ''),
|
|
'remark' => (string) ($row['remark'] ?? ''),
|
|
'add_fans_count' => (int) ($row['add_fans_count'] ?? 0),
|
|
'total_open_count' => (int) ($row['total_open_count'] ?? 0),
|
|
'unreplied_count' => (int) ($row['unreplied_count'] ?? 0),
|
|
'paid_appointment_count' => (int) ($row['paid_appointment_count'] ?? 0),
|
|
'free_appointment_count' => (int) ($row['free_appointment_count'] ?? 0),
|
|
'interview_count' => (int) ($row['interview_count'] ?? 0),
|
|
'order_amount' => round((float) ($row['order_amount'] ?? 0), 2),
|
|
'completed_order_count' => (int) ($row['completed_order_count'] ?? 0),
|
|
'account_cost' => 0.0,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
private static function emptyMetrics(): array
|
|
{
|
|
return [
|
|
'add_fans_count' => 0,
|
|
'total_open_count' => 0,
|
|
'unreplied_count' => 0,
|
|
'paid_appointment_count' => 0,
|
|
'free_appointment_count' => 0,
|
|
'appointment_total_count' => 0,
|
|
'interview_count' => 0,
|
|
'order_amount' => 0.0,
|
|
'completed_order_count' => 0,
|
|
'account_cost' => 0.0,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $base
|
|
* @param array<string, mixed> $row
|
|
* @return array<string, mixed>
|
|
*/
|
|
private static function accumulateMetrics(array $base, array $row): array
|
|
{
|
|
$base['add_fans_count'] += (int) ($row['add_fans_count'] ?? 0);
|
|
$base['total_open_count'] += (int) ($row['total_open_count'] ?? 0);
|
|
$base['unreplied_count'] += (int) ($row['unreplied_count'] ?? 0);
|
|
$base['paid_appointment_count'] += (int) ($row['paid_appointment_count'] ?? 0);
|
|
$base['free_appointment_count'] += (int) ($row['free_appointment_count'] ?? 0);
|
|
$base['interview_count'] += (int) ($row['interview_count'] ?? 0);
|
|
$base['order_amount'] = round((float) $base['order_amount'] + (float) ($row['order_amount'] ?? 0), 2);
|
|
$base['completed_order_count'] += (int) ($row['completed_order_count'] ?? 0);
|
|
$base['account_cost'] = round((float) $base['account_cost'] + (float) ($row['account_cost'] ?? 0), 2);
|
|
|
|
return $base;
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $entity
|
|
* @return array<string, mixed>
|
|
*/
|
|
private static function finalizeMetrics(array $entity): array
|
|
{
|
|
$paidAppointmentCount = (int) ($entity['paid_appointment_count'] ?? 0);
|
|
$freeAppointmentCount = (int) ($entity['free_appointment_count'] ?? 0);
|
|
$appointmentTotalCount = $paidAppointmentCount + $freeAppointmentCount;
|
|
$interviewCount = (int) ($entity['interview_count'] ?? 0);
|
|
$addFansCount = (int) ($entity['add_fans_count'] ?? 0);
|
|
$totalOpenCount = (int) ($entity['total_open_count'] ?? 0);
|
|
$completedOrderCount = (int) ($entity['completed_order_count'] ?? 0);
|
|
$orderAmount = round((float) ($entity['order_amount'] ?? 0), 2);
|
|
$accountCost = round((float) ($entity['account_cost'] ?? 0), 2);
|
|
|
|
$entity['appointment_total_count'] = $appointmentTotalCount;
|
|
$entity['order_amount'] = $orderAmount;
|
|
$entity['account_cost'] = $accountCost;
|
|
$entity['total_open_rate'] = self::percent($totalOpenCount, $addFansCount);
|
|
$entity['paid_appointment_rate'] = self::percent($paidAppointmentCount, $addFansCount);
|
|
$entity['open_appointment_rate'] = self::percent($paidAppointmentCount, $totalOpenCount);
|
|
$entity['interview_rate'] = self::percent($interviewCount, $appointmentTotalCount);
|
|
$entity['receive_rate'] = self::percent($completedOrderCount, $addFansCount);
|
|
$entity['interview_receive_rate'] = self::percent($completedOrderCount, $interviewCount);
|
|
$entity['open_receive_rate'] = self::percent($completedOrderCount, $totalOpenCount);
|
|
$entity['avg_unit_price'] = self::safeDivideMoney($orderAmount, $completedOrderCount);
|
|
$entity['cash_cost'] = self::safeDivideMoney($accountCost, $addFansCount);
|
|
$entity['roi'] = self::safeDivideRatio($orderAmount, $accountCost);
|
|
|
|
return $entity;
|
|
}
|
|
|
|
/**
|
|
* @return array{0: string, 1: string}
|
|
*/
|
|
private static function resolveTimeRange(array $params): array
|
|
{
|
|
$today = date('Y-m-d');
|
|
$timeType = (string) ($params['time_type'] ?? 'today');
|
|
|
|
switch ($timeType) {
|
|
case 'yesterday':
|
|
$startDate = date('Y-m-d', strtotime('-1 day'));
|
|
$endDate = $startDate;
|
|
break;
|
|
case 'week':
|
|
$startDate = date('Y-m-d', strtotime('-6 days'));
|
|
$endDate = $today;
|
|
break;
|
|
case 'month':
|
|
$startDate = date('Y-m-d', strtotime('-29 days'));
|
|
$endDate = $today;
|
|
break;
|
|
case 'custom':
|
|
$startDate = trim((string) ($params['start_date'] ?? ''));
|
|
$endDate = trim((string) ($params['end_date'] ?? ''));
|
|
if ($startDate === '' || $endDate === '') {
|
|
$startDate = $today;
|
|
$endDate = $today;
|
|
} elseif ($startDate > $endDate) {
|
|
[$startDate, $endDate] = [$endDate, $startDate];
|
|
}
|
|
break;
|
|
case 'today':
|
|
default:
|
|
$startDate = $today;
|
|
$endDate = $today;
|
|
}
|
|
|
|
return [$startDate, $endDate];
|
|
}
|
|
|
|
private static function percent(int $numerator, int $denominator): float
|
|
{
|
|
if ($denominator <= 0) {
|
|
return 0.0;
|
|
}
|
|
|
|
return round(($numerator / $denominator) * 100, 2);
|
|
}
|
|
|
|
private static function safeDivideMoney(float $numerator, int $denominator): float
|
|
{
|
|
if ($denominator <= 0) {
|
|
return 0.0;
|
|
}
|
|
|
|
return round($numerator / $denominator, 2);
|
|
}
|
|
|
|
private static function safeDivideRatio(float $numerator, float $denominator): float
|
|
{
|
|
if ($denominator <= 0) {
|
|
return 0.0;
|
|
}
|
|
|
|
return round($numerator / $denominator, 2);
|
|
}
|
|
}
|