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 $entity * @return array */ 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 */ 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|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|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|null $effectiveAdminIds * @return array */ 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 $row * @return array */ 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 */ 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 $base * @param array $row * @return array */ 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 $entity * @return array */ 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); } }