webman_ad/app/service/AdsInsightService.php

2126 lines
112 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace app\service;
use app\model\Ad;
use app\model\BpsAdCreativeInsight;
use app\model\BpsAdInsight;
//use app\model\BpsAdsMerchantRelation;
use app\model\BpsAdsMerchantRelation;
use app\model\BpsAdsMerchantStoreRelation;
use app\model\DayData;
use app\model\Campaign;
use app\model\BpsAdCampaign;
use app\model\BpsAdSet;
use app\model\BpsAdAd;
use app\model\ThirdUser;
use app\model\Asset;
use app\model\AssetRelation;
use app\model\ThirdUserAdvertiser;
use DateTime;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
use think\db\exception\DbException;
use think\facade\Db as ThinkDb;
use support\Redis;
use support\Response;
use support\Log;
class AdsInsightService
{
// 在类顶部添加
private static $sqlLogEnabled = false;
// 状态映射数组
private static $statusMapping = [
0 => 'UNSPECIFIED', // UNSPECIFIED
1 => 'UNKNOWN', // UNKNOWN
2 => 'ENABLED', // ENABLED
3 => 'PAUSED', // PAUSED
4 => 'REMOVED', // REMOVED
5 => 'FROZEN', // FROZEN
6 => 'ARCHIVED', // ARCHIVED
];
// 状态映射数组
private static $platformMapping = [
1 => 'meta',
2 => 'google',
3 => 'tiktok',
];
// 状态映射数组
private static $creativeTypeMapping = [
1 => 'image',
2 => 'video',
];
// Redis 键名前缀
const REDIS_KEY_PREFIX = 'ad_data_count:';
/**
* 获取广告数据的各项总数(统一接口)
*
* @param array $customerIds 客户 ID 数组
* @return array 各项 count 统计数据
*/
public static function getAdCountData($merchantId, $storeId, $customerIds, $startDate, $endDate)
{
if (empty($customerIds)) {
return [];
}
// 生成唯一的哈希键
$hashKey = self::generateHashKey($customerIds);
$redisKey = self::REDIS_KEY_PREFIX . $hashKey;
// 尝试从 Redis 中获取缓存值
$cachedData = Redis::get($redisKey);
if (!empty($cachedData)) {
return json_decode($cachedData, true);
}
// 没有缓存时重新计算
$countData = self::calculateCountData($merchantId, $storeId, $customerIds, $startDate, $endDate);
// 缓存到 Redis有效期 10 分钟
Redis::setex($redisKey, 600, json_encode($countData));
return $countData;
}
/**
* 计算广告数据的 count
*/
protected static function calculateCountData($merchantId, $storeId, $customerIds, $startDate, $endDate)
{
if (!$startDate || !$endDate) {
[$startDate, $endDate] = self::getLastWeekDateRange();
}
return [
'account_list_count' => self::getAccountListCount($merchantId, $storeId, $customerIds, $startDate, $endDate),
'campaign_list_count' => self::getCampaignListCount($customerIds, $startDate, $endDate),
'adset_list_count' => self::getAdsetListCount($customerIds, $startDate, $endDate),
'ad_list_count' => self::getAdListCount($customerIds, $startDate, $endDate),
'creative_list_count' => self::getCreativeListCount($customerIds, $startDate, $endDate),
];
}
/**
* 生成 Redis 键的唯一哈希值
*/
protected static function generateHashKey(array $customerIds)
{
sort($customerIds);
return md5(json_encode($customerIds));
}
/**
* 获取广告列表的 count
*/
protected static function getAdListCount(array $customerIds, $startDate, $endDate)
{
return self::getAdList(0, $customerIds, 1, 1, '', $startDate, $endDate, 0, false, true);
}
/**
* 获取广告系列列表的 count
*/
protected static function getCampaignListCount(array $customerIds, $startDate, $endDate)
{
return self::getCampaignList(0, $customerIds, 1, 1, '', $startDate, $endDate, 0, false, true);
}
/**
* 获取广告组列表的 count
*/
protected static function getAdsetListCount(array $customerIds, $startDate, $endDate)
{
return self::getAdsetList(0, $customerIds, 1, 1, '', $startDate, $endDate, 0, false, true);
}
/**
* 获取账户列表的 count
*/
protected static function getAccountListCount($merchantId, $storeId, $customerIds, $startDate, $endDate)
{
return self::getAccountList(0, $merchantId, $storeId, $customerIds, 1, 1, '', $startDate, $endDate, false, true);
}
/**
* 获取账户列表的 count
*/
protected static function getCreativeListCount(array $customerIds, $startDate, $endDate)
{
return self::getCreativeInsightData(0, $customerIds, 1, 1, '', $startDate, $endDate, false, true);
}
/**
* 获取最近 7 天的起始和结束日期
*/
protected static function getLastWeekDateRange()
{
$endDate = (int)date('Ymd');
$startDate = (int)date('Ymd', strtotime('-6 days'));
return [$startDate, $endDate];
}
/**
* 获取广告系列列表
*/
public static function getCampaignList($platformType, $customerIds, $page, $pageSize, $keyword, $startDate = null, $endDate = null, $status = 0, $requireSpend = false, $countOnly = false)
{
// 检查 customerIds 是否为空,直接返回空结构
if (empty($customerIds)) {
return $countOnly ? 0 : [
'pagination' => [
'startIndex' => 0,
'maxResults' => 0,
'count' => 0,
'pageNo' => $page,
'pageSize' => $pageSize,
'pages' => 0,
],
'statistics' => [],
'data' => [],
];
}
// 基础查询:广告活动和日数据表联接
$query = BpsAdCampaign::alias('c')
->cache(false) // 强制不使用缓存
->where('c.account_id', 'in', $customerIds);
// 如果只需要记录条数,执行计数查询
if ($countOnly) {
return $query->count();
}
// 动态构建日期条件
$dateCondition = '';
if ($startDate && $endDate) {
$dateCondition = "d.date BETWEEN '{$startDate}' AND '{$endDate}'";
}
$query->leftJoin('bps.bps_ads_insights d', "c.campaign_id = d.ad_campaign_id AND c.platform_type = d.platform AND {$dateCondition}")
->field('c.campaign_id, c.status as status, c.name, c.account_id,c.platform_type,
COALESCE(SUM(d.clicks), 0) as clicks,
COALESCE(SUM(d.spend) / 1000000, 0) as spend,
COALESCE(SUM(d.impressions), 0) as impressions,
COALESCE(SUM(d.adds_to_cart), 0) as adds_to_cart,
COALESCE(SUM(d.purchases), 0) as purchases,
COALESCE(SUM(d.platform_purchase), 0) as platform_purchase,
COALESCE(SUM(d.pixel_purchase), 0) as pixel_purchase,
COALESCE(SUM(d.purchases_value / 1000000), 0) as purchases_value,
COALESCE(SUM(d.platform_purchase_value / 1000000), 0) as revenue,
COALESCE(SUM(d.total_cost / 1000000), 0) as total_cost,
-1 as conversion_rate, -1 as roas, -1 as ctr,-1 as net_profit,-1 as net_profit_margin,-1 as net_profit_on_ad_spend')
->group('c.campaign_id, c.status, c.account_id, c.name,c.platform_type');
// ->where('c.account_id', 'in', $customerIds); // 添加 customerIds 条件
// 根据 $requireSpend 参数决定是否添加 SUM(d.spend) > 0 的条件
if ($requireSpend) {
$query->having('SUM(d.spend) > 0');
}
if ($status !== 0) {
$query->where('c.status', '=', $status);
}
// 添加关键字过滤条件
$query->where(function ($query) use ($keyword, $platformType) {
if ($keyword) {
$query->whereRaw('LOWER(c.name) LIKE ?', ['%' . strtolower($keyword) . '%']);
}
if ($platformType) {
$platformType = (int)$platformType;
$query->where('c.platform_type', '=', $platformType);
}
});
$query->order('c.platform_type,c.account_id,c.status', 'asc');
// 获取所有符合条件的数据(不分页)
$allCampaigns = $query->select()->toArray();
$total_spend = array_sum(array_column($allCampaigns, 'spend'));
$total_cost = array_sum(array_column($allCampaigns, 'total_cost'));
$total_impressions = array_sum(array_column($allCampaigns, 'impressions'));
$total_clicks = array_sum(array_column($allCampaigns, 'clicks'));
$total_purchases_value = array_sum(array_column($allCampaigns, 'purchases_value'));
$total_revenue = array_sum(array_column($allCampaigns, 'revenue'));
$total_purchases = array_sum(array_column($allCampaigns, 'purchases'));
// $total_platform_purchase = array_sum(array_column($allCampaigns, 'platform_purchase'));
// $total_pixel_purchase = array_sum(array_column($allCampaigns, 'pixel_purchase'));
$adds_to_cart = array_sum(array_column($allCampaigns, 'adds_to_cart'));
$cost_per_purchase = $total_purchases == 0 ? 0 : round($total_spend / $total_purchases, 2);
// 汇总统计数据
$statistics = [
'platform_purchase' => number_format(array_sum(array_column($allCampaigns, 'platform_purchase'))),
'pixel_purchase' => number_format(array_sum(array_column($allCampaigns, 'pixel_purchase'))),
'roas' => $total_spend == 0 ? '-' : round($total_revenue / $total_spend, 2) . 'x',
'amount_spend' => '$' . number_format($total_spend, 2) ?: '$0.00', // 格式化支出
'clicks' => number_format($total_clicks),
'impressions' => number_format($total_impressions),
'adds_to_cart' => number_format($adds_to_cart),
'cost_per_atc' => $adds_to_cart == 0 ? '-' : '$' . number_format(($total_spend / $adds_to_cart), 2),
'purchases' => number_format($total_purchases),
'purchases_value' => array_sum(array_column($allCampaigns, 'purchases_value')),
'cost_per_purchase' => '$' . number_format($cost_per_purchase, 2) ?: '$0.00',
'revenue' => '$' . number_format(array_sum(array_column($allCampaigns, 'revenue')), 2) ?: '$0.00', // 格式化收入
'total_cost' => '$' . number_format($total_cost, 2) ?: '$0.00', // 格式化总成本
'conversion_rate' => $total_clicks == 0 ? '-' : round(($total_purchases / $total_clicks) * 100, 2) . '%', // 转换率
'net_profit' => ($total_revenue - $total_cost) >= 0 ? '+$' . number_format($total_revenue - $total_cost, 2) : '-$' . number_format(abs($total_revenue - $total_cost), 2), // 净利润
'net_profit_margin' => $total_revenue == 0 ? '-' : round(($total_revenue - $total_cost) / $total_revenue, 2) * 100 . '%', // 净利润率
'net_profit_on_ad_spend' => $total_spend == 0 ? '-' : round(($total_revenue - $total_cost) / $total_spend, 2) * 100 . '%', // 广告支出净利润
// 计算总的 CTR
'ctr' => ($total_impressions > 0) ? number_format(($total_clicks / $total_impressions) * 100, 2) . '%' : '-', // 格式化为百分比
];
// 获取分页数据
// $campaigns = $query->paginate($pageSize, false, ['page' => $page]);
// 获取分页数据
$page = max(1, (int)$page); // 确保页码不小于 1
// $ads = $query->page($page, $pageSize)->select();
$campaigns = $query->limit(($page - 1) * $pageSize, $pageSize)->select();
$accountIds = array_unique(array_column($campaigns->toArray(), 'account_id'));
$accountNames = BpsAdsMerchantRelation::whereIn('account_id', $accountIds)
->column('account_name', 'account_id');
// 确保数据格式统一
$result = array_map(function ($item) use ($accountNames){
// CTR 的计算:点击率 = 点击数 / 展示数
$ctr = $item['impressions'] > 0 ? number_format(($item['clicks'] / $item['impressions']) * 100, 2) . '%' : '-';
// Conversion Rate 的计算:转换率 = 购买数 / 点击数
$conversion_rate = $item['clicks'] > 0 ? round(($item['purchases'] / $item['clicks']) * 100, 2) . '%' : '-';
// Net Profit 的计算:净利润 = 收入 - 总成本
$net_profit = ($item['revenue'] - $item['total_cost']) >= 0 ? '+$' . number_format($item['revenue'] - $item['total_cost'], 2) : '-$' . number_format(abs($item['revenue'] - $item['total_cost']), 2);
// Net Profit Margin 的计算:净利润率 = 净利润 / 收入
$net_profit_margin = $item['revenue'] > 0 ? round(($item['revenue'] - $item['total_cost']) / $item['revenue'], 2) * 100 . '%' : '-';
// Net Profit on Ad Spend 的计算:广告支出净利润 = (收入 - 总成本) / 广告支出
$net_profit_on_ad_spend = $item['spend'] > 0 ? round(($item['revenue'] - $item['total_cost']) / $item['spend'], 2) * 100 . '%' : '-';
return [
'id' => $item['campaign_id'],
'platform_type' => $item['platform_type'],
'account_id' => $item['account_id'], // 映射为 customer_id
'account_name' => $accountNames[$item['account_id']] ?? '-', // 新增字段
'name' => $item['name'] ?: '-', // 若 name 为空则显示 '-'
'status' => $item['status'],
'platform_purchase' => number_format($item['platform_purchase']),
'pixel_purchase' => number_format($item['pixel_purchase']),
'roas' => $item['spend'] == 0 ? '-' : round($item['revenue'] / $item['spend'], 2) . 'x',
'spend' => '$' . number_format($item['spend'], 2), // 格式化支出
'impressions' => number_format($item['impressions']),
'clicks' => number_format($item['clicks']),
'ctr' => $ctr, // CTR 字段
'adds_to_cart' => number_format($item['adds_to_cart']),
'cost_per_atc' => $item['adds_to_cart'] > 0 ? '$' . number_format(($item['spend'] / $item['adds_to_cart']), 2) : '-',
'purchases' => number_format($item['purchases']),
'purchases_value' => '$' . number_format($item['purchases_value'], 2), // 格式化购买金额
'cost_per_purchase' => $item['purchases'] > 0 ? '$' . number_format(($item['spend'] / $item['purchases']), 2) : '-',
'revenue' => '$' . number_format($item['revenue'], 2), // 格式化收入
'total_cost' => '$' . number_format($item['total_cost'], 2), // 格式化总成本
'conversion_rate' => $conversion_rate,
'net_profit' => $net_profit,
'net_profit_margin' => $net_profit_margin,
'net_profit_on_ad_spend' => $net_profit_on_ad_spend,
];
}, $campaigns->toArray());
// Pagination 数据
$pagination = [
'startIndex' => ($page - 1) * $pageSize + 1, // 确保索引从 1 开始
'maxResults' => count($campaigns), // 当前页实际返回数据数量
'count' => count($allCampaigns), // 符合条件的总记录数
'pageNo' => $page, // 当前页码
'pageSize' => $pageSize, // 每页条数
'pages' => (int)ceil(count($allCampaigns) / $pageSize), // 总页数
];
return [
'pagination' => $pagination,
'statistics' => $statistics,
'data' => $result,
];
}
/**
* 导出广告系列列表到 Excel
*
* @param int $platformType 平台类型
* @param array $customerIds 客户ID列表
* @param int $page 当前页码
* @param int $pageSize 每页条数
* @param string $keyword 关键字
* @param string|null $startDate 开始日期
* @param string|null $endDate 结束日期
* @param int $status 广告系列状态
*/
public static function exportCampaignsToExcel($platformType, $customerIds, $page, $pageSize, $keyword, $startDate = null, $endDate = null, $status = 0, $requireSpend = false)
{
// 调用 getCampaignList 获取广告系列数据
$campaignList = self::getCampaignList($platformType, $customerIds, $page, $pageSize, $keyword, $startDate, $endDate, $status, $requireSpend);
if (empty($campaignList['data'])) {
$data = [
'code' => 901,
'msg' => 'No data available for export.',
'data' => []
];
return new Response(400, ['Content-Type' => 'application/json'], json_encode($data, JSON_UNESCAPED_UNICODE));
}
// 创建 Spreadsheet 对象
$spreadsheet = new Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
// 设置表头
$headers = [
'A1' => 'Ad Campaigns',
'B1' => 'Status',
'C1' => 'Ad Platform', // 修正为 Ad Platform
'D1' => 'Ad Accounts', // 修正为 Ad Accounts
'E1' => 'Platform Purchases',
'F1' => 'Best Pixel Purchases',
'G1' => 'ROAS',
'H1' => 'Amount Spent',
'I1' => 'Impressions',
'J1' => 'Clicks',
'K1' => 'Click Through Rate',
'L1' => 'Adds to Cart',
'M1' => 'Cost per ATC',
'N1' => 'Purchases',
'O1' => 'Purchases Value',
'P1' => 'Cost per Purchase',
'Q1' => 'Conversion Rate',
'R1' => 'Revenue',
'S1' => 'Total Cost',
'T1' => 'Net Profit',
'U1' => 'Net Profit Margin',
'V1' => 'Net Profit on Ad Spend' // 新增 V 列
];
foreach ($headers as $cell => $header) {
$sheet->setCellValue($cell, $header);
}
// 填充数据
$row = 2;
foreach ($campaignList['data'] as $campaign) {
// $sheet->setCellValueExplicit('A' . $row, (string)$campaign['campaign_id'], \PhpOffice\PhpSpreadsheet\Cell\DataType::TYPE_STRING);
$sheet->setCellValue('A' . $row, $campaign['name']); // Ad Campaigns
$sheet->setCellValue('B' . $row, self::$statusMapping[$campaign['status']]); // Status
$sheet->setCellValue('C' . $row, self::$platformMapping[$campaign['platform_type']]); // Platform Type
// $sheet->setCellValueExplicit('D' . $row, (string)$campaign['account_id'], \PhpOffice\PhpSpreadsheet\Cell\DataType::TYPE_STRING);
$sheet->setCellValue('D' . $row, $campaign['account_name']);
$sheet->setCellValue('E' . $row, $campaign['platform_purchase']);
$sheet->setCellValue('F' . $row, $campaign['pixel_purchase']);
$sheet->setCellValue('G' . $row, $campaign['roas']); // ROAS
$sheet->setCellValue('H' . $row, $campaign['spend']); // Amount Spent
$sheet->setCellValue('I' . $row, $campaign['impressions']); // Impressions
$sheet->setCellValue('J' . $row, $campaign['clicks']); // Clicks
$sheet->setCellValue('K' . $row, $campaign['ctr']); // Click Through Rate
$sheet->setCellValue('L' . $row, $campaign['adds_to_cart']); // Adds to Cart
$sheet->setCellValue('M' . $row, $campaign['cost_per_atc']); // Cost per ATC
$sheet->setCellValue('N' . $row, $campaign['purchases']); // Purchases
$sheet->setCellValue('O' . $row, $campaign['purchases_value']); // Purchases Value
$sheet->setCellValue('P' . $row, $campaign['cost_per_purchase']); // Cost per Purchase
$sheet->setCellValue('Q' . $row, $campaign['conversion_rate']); // Conversion Rate
$sheet->setCellValue('R' . $row, $campaign['revenue']); // Revenue
$sheet->setCellValue('S' . $row, $campaign['total_cost']); // Total Cost
$sheet->setCellValue('T' . $row, $campaign['net_profit']); // Net Profit
$sheet->setCellValue('U' . $row, $campaign['net_profit_margin']); // Net Profit Margin
$sheet->setCellValue('V' . $row, $campaign['net_profit_on_ad_spend']); // Net Profit on Ad Spend
$row++;
}
// 填充统计信息到表格的最后一行
$statistics = $campaignList['statistics'];
$statisticsRow = $row; // 统计信息从当前行开始
$sheet->setCellValue('D' . $statisticsRow, 'Totals'); // 统计信息标题
$sheet->setCellValue('E' . $statisticsRow, $statistics['platform_purchase']); // Platform Purchases
$sheet->setCellValue('F' . $statisticsRow, $statistics['pixel_purchase']); // Pixel Purchases
$sheet->setCellValue('G' . $statisticsRow, $statistics['roas']); // ROAS
$sheet->setCellValue('H' . $statisticsRow, $statistics['amount_spend']); // Amount Spent
$sheet->setCellValue('I' . $statisticsRow, $statistics['impressions']); // Impressions
$sheet->setCellValue('J' . $statisticsRow, $statistics['clicks']); // Clicks
$sheet->setCellValue('K' . $statisticsRow, $statistics['ctr']); // Click Through Rate
$sheet->setCellValue('L' . $statisticsRow, $statistics['adds_to_cart']); // Adds to Cart
$sheet->setCellValue('M' . $statisticsRow, $statistics['cost_per_atc']); // Cost per ATC
$sheet->setCellValue('N' . $statisticsRow, $statistics['purchases']); // Purchases
$sheet->setCellValue('O' . $statisticsRow, $statistics['purchases_value']); // Purchases Value
$sheet->setCellValue('P' . $statisticsRow, $statistics['cost_per_purchase']); // Cost per Purchase
$sheet->setCellValue('Q' . $statisticsRow, $statistics['conversion_rate']); // Conversion Rate
$sheet->setCellValue('R' . $statisticsRow, $statistics['revenue']); // Revenue
$sheet->setCellValue('S' . $statisticsRow, $statistics['total_cost']); // Total Cost
$sheet->setCellValue('T' . $statisticsRow, $statistics['net_profit']); // Net Profit
$sheet->setCellValue('U' . $statisticsRow, $statistics['net_profit_margin']); // Net Profit Margin
$sheet->setCellValue('V' . $statisticsRow, $statistics['net_profit_on_ad_spend']); // Net Profit on Ad Spend
unset($statistics);
unset($campaignList);
// 文件名与路径
$fileName = 'Exported_Campaigns_' . date('Y-m-d_H-i-s') . '.xlsx';
$filePath = public_path() . '/' . $fileName;
// 保存 Excel 文件
$writer = new Xlsx($spreadsheet);
try {
$writer->save($filePath);
return response()->download($filePath, $fileName);
} catch (\Exception $e) {
// return ['success' => false, 'message' => $e->getMessage()];
$data = [
'code' => 901,
'msg' => 'No data available for export.',
'data' => []
];
return new Response(400, ['Content-Type' => 'application/json'], json_encode($data, JSON_UNESCAPED_UNICODE));
}
}
/**
* 导出广告组列表到 Excel
*
* @param int $platformType 平台类型
* @param array $customerIds 客户ID列表
* @param int $page 当前页码
* @param int $pageSize 每页条数
* @param string $keyword 关键字
* @param string|null $startDate 开始日期
* @param string|null $endDate 结束日期
* @param int $status 广告系列状态
*/
public static function exportAdsetsToExcel($platformType, $customerIds, $page, $pageSize, $keyword, $startDate = null, $endDate = null, $status = 0, $requireSpend = false)
{
// 调用 getCampaignList 获取广告系列数据
$adsetList = self::getAdsetList($platformType, $customerIds, $page, $pageSize, $keyword, $startDate, $endDate, $status, $requireSpend);
if (empty($adsetList['data'])) {
$data = [
'code' => 901,
'msg' => 'No data available for export.',
'data' => []
];
return new Response(400, ['Content-Type' => 'application/json'], json_encode($data, JSON_UNESCAPED_UNICODE));
}
// 创建 Spreadsheet 对象
$spreadsheet = new Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
// 设置表头
$headers = [
'A1' => 'Ad Sets',
'B1' => 'Status',
'C1' => 'Ad Platform', // 修正为 Ad Platform
'D1' => 'Ad Campaigns',
'E1' => 'Platform Purchases',
'F1' => 'Best Pixel Purchases',
'G1' => 'ROAS',
'H1' => 'Amount Spent',
'I1' => 'Impressions',
'J1' => 'Clicks',
'K1' => 'Click Through Rate',
'L1' => 'Adds to Cart',
'M1' => 'Cost per ATC',
'N1' => 'Purchases',
'O1' => 'Purchases Value',
'P1' => 'Cost per Purchase',
'Q1' => 'Conversion Rate',
'R1' => 'Revenue',
'S1' => 'Total Cost',
'T1' => 'Net Profit',
'U1' => 'Net Profit Margin',
'V1' => 'Net Profit on Ad Spend' // 新增 V 列
];
foreach ($headers as $cell => $header) {
$sheet->setCellValue($cell, $header);
}
// 填充数据
$row = 2;
foreach ($adsetList['data'] as $adset) {
// $sheet->setCellValueExplicit('A' . $row, (string)$campaign['campaign_id'], \PhpOffice\PhpSpreadsheet\Cell\DataType::TYPE_STRING);
$sheet->setCellValue('A' . $row, $adset['name']); // Ad Campaigns
$sheet->setCellValue('B' . $row, self::$statusMapping[$adset['status']]); // Status
$sheet->setCellValue('C' . $row, self::$platformMapping[$adset['platform_type']]); // Platform Type
// $sheet->setCellValueExplicit('D' . $row, (string)$adset['account_id'], \PhpOffice\PhpSpreadsheet\Cell\DataType::TYPE_STRING);
$sheet->setCellValue('D' . $row, $adset['campaign_name']);
$sheet->setCellValue('E' . $row, $adset['platform_purchase']);
$sheet->setCellValue('F' . $row, $adset['pixel_purchase']);
$sheet->setCellValue('G' . $row, $adset['roas']); // ROAS
$sheet->setCellValue('H' . $row, $adset['spend']); // Amount Spent
$sheet->setCellValue('I' . $row, $adset['impressions']); // Impressions
$sheet->setCellValue('J' . $row, $adset['clicks']); // Clicks
$sheet->setCellValue('K' . $row, $adset['ctr']); // Click Through Rate
$sheet->setCellValue('L' . $row, $adset['adds_to_cart']); // Adds to Cart
$sheet->setCellValue('M' . $row, $adset['cost_per_atc']); // Cost per ATC
$sheet->setCellValue('N' . $row, $adset['purchases']); // Purchases
$sheet->setCellValue('O' . $row, $adset['purchases_value']); // Purchases Value
$sheet->setCellValue('P' . $row, $adset['cost_per_purchase']); // Cost per Purchase
$sheet->setCellValue('Q' . $row, $adset['conversion_rate']); // Conversion Rate
$sheet->setCellValue('R' . $row, $adset['revenue']); // Revenue
$sheet->setCellValue('S' . $row, $adset['total_cost']); // Total Cost
$sheet->setCellValue('T' . $row, $adset['net_profit']); // Net Profit
$sheet->setCellValue('U' . $row, $adset['net_profit_margin']); // Net Profit Margin
$sheet->setCellValue('V' . $row, $adset['net_profit_on_ad_spend']); // Net Profit on Ad Spend
$row++;
}
// 填充统计信息到表格的最后一行
$statistics = $adsetList['statistics'];
$statisticsRow = $row; // 统计信息从当前行开始
$sheet->setCellValue('D' . $statisticsRow, 'Totals'); // 统计信息标题
$sheet->setCellValue('E' . $statisticsRow, $statistics['platform_purchase']);
$sheet->setCellValue('F' . $statisticsRow, $statistics['pixel_purchase']);
$sheet->setCellValue('G' . $statisticsRow, $statistics['roas']); // ROAS
$sheet->setCellValue('H' . $statisticsRow, $statistics['amount_spend']); // Amount Spent
$sheet->setCellValue('I' . $statisticsRow, $statistics['impressions']); // Impressions
$sheet->setCellValue('J' . $statisticsRow, $statistics['clicks']); // Clicks
$sheet->setCellValue('K' . $statisticsRow, $statistics['ctr']); // Click Through Rate
$sheet->setCellValue('L' . $statisticsRow, $statistics['adds_to_cart']); // Adds to Cart
$sheet->setCellValue('M' . $statisticsRow, $statistics['cost_per_atc']); // Cost per ATC
$sheet->setCellValue('N' . $statisticsRow, $statistics['purchases']); // Purchases
$sheet->setCellValue('O' . $statisticsRow, $statistics['purchases_value']); // Purchases Value
$sheet->setCellValue('P' . $statisticsRow, $statistics['cost_per_purchase']); // Cost per Purchase
$sheet->setCellValue('Q' . $statisticsRow, $statistics['conversion_rate']); // Conversion Rate
$sheet->setCellValue('R' . $statisticsRow, $statistics['revenue']); // Revenue
$sheet->setCellValue('S' . $statisticsRow, $statistics['total_cost']); // Total Cost
$sheet->setCellValue('T' . $statisticsRow, $statistics['net_profit']); // Net Profit
$sheet->setCellValue('U' . $statisticsRow, $statistics['net_profit_margin']); // Net Profit Margin
$sheet->setCellValue('V' . $statisticsRow, $statistics['net_profit_on_ad_spend']); // Net Profit on Ad Spend
unset($statistics);
unset($adsetList);
// 文件名与路径
$fileName = 'Exported_Adsets_' . date('Y-m-d_H-i-s') . '.xlsx';
$filePath = public_path() . '/' . $fileName;
// 保存 Excel 文件
$writer = new Xlsx($spreadsheet);
try {
$writer->save($filePath);
return response()->download($filePath, $fileName);
} catch (\Exception $e) {
// return ['success' => false, 'message' => $e->getMessage()];
$data = [
'code' => 901,
'msg' => 'No data available for export.',
'data' => []
];
return new Response(400, ['Content-Type' => 'application/json'], json_encode($data, JSON_UNESCAPED_UNICODE));
}
}
/**
* 导出广告列表到 Excel
*
* @param int $platformType 平台类型
* @param array $customerIds 客户ID列表
* @param int $page 当前页码
* @param int $pageSize 每页条数
* @param string $keyword 关键字
* @param string|null $startDate 开始日期
* @param string|null $endDate 结束日期
* @param int $status 广告系列状态
*/
public static function exportAdsToExcel($platformType, $customerIds, $page, $pageSize, $keyword, $startDate = null, $endDate = null, $status = 0, $requireSpend = false)
{
// 调用 getCampaignList 获取广告系列数据
$adList = self::getAdList($platformType, $customerIds, $page, $pageSize, $keyword, $startDate, $endDate, $status, $requireSpend, false);
if (empty($adList['data'])) {
$data = [
'code' => 901,
'msg' => 'No data available for export.',
'data' => []
];
return new Response(400, ['Content-Type' => 'application/json'], json_encode($data, JSON_UNESCAPED_UNICODE));
}
// 创建 Spreadsheet 对象
$spreadsheet = new Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
// 设置表头
$headers = [
'A1' => 'Ads',
'B1' => 'Status',
'C1' => 'Ad Platform', // 修正为 Ad Platform
'D1' => 'Ad Sets',
'E1' => 'Platform Purchases',
'F1' => 'Best Pixel Purchases',
'G1' => 'ROAS',
'H1' => 'Amount Spent',
'I1' => 'Impressions',
'J1' => 'Clicks',
'K1' => 'Click Through Rate',
'L1' => 'Adds to Cart',
'M1' => 'Cost per ATC',
'N1' => 'Purchases',
'O1' => 'Purchases Value',
'P1' => 'Cost per Purchase',
'Q1' => 'Conversion Rate',
'R1' => 'Revenue',
'S1' => 'Total Cost',
'T1' => 'Net Profit',
'U1' => 'Net Profit Margin',
'V1' => 'Net Profit on Ad Spend' // 新增 V 列
];
foreach ($headers as $cell => $header) {
$sheet->setCellValue($cell, $header);
}
// 填充数据
$row = 2;
foreach ($adList['data'] as $ad) {
// $sheet->setCellValueExplicit('A' . $row, (string)$campaign['campaign_id'], \PhpOffice\PhpSpreadsheet\Cell\DataType::TYPE_STRING);
$sheet->setCellValue('A' . $row, $ad['name']); // Ad Campaigns
$sheet->setCellValue('B' . $row, self::$statusMapping[$ad['status']]); // Status
$sheet->setCellValue('C' . $row, self::$platformMapping[$ad['platform_type']]); // Platform Type
// $sheet->setCellValueExplicit('D' . $row, (string)$ad['account_id'], \PhpOffice\PhpSpreadsheet\Cell\DataType::TYPE_STRING);
$sheet->setCellValue('D' . $row, $ad['ad_set_name']);
$sheet->setCellValue('E' . $row, $ad['platform_purchase']);
$sheet->setCellValue('F' . $row, $ad['pixel_purchase']);
$sheet->setCellValue('G' . $row, $ad['roas']); // ROAS
$sheet->setCellValue('H' . $row, $ad['spend']); // Amount Spent
$sheet->setCellValue('I' . $row, $ad['impressions']); // Impressions
$sheet->setCellValue('J' . $row, $ad['clicks']); // Clicks
$sheet->setCellValue('K' . $row, $ad['ctr']); // Click Through Rate
$sheet->setCellValue('L' . $row, $ad['adds_to_cart']); // Adds to Cart
$sheet->setCellValue('M' . $row, $ad['cost_per_atc']); // Cost per ATC
$sheet->setCellValue('N' . $row, $ad['purchases']); // Purchases
$sheet->setCellValue('O' . $row, $ad['purchases_value']); // Purchases Value
$sheet->setCellValue('P' . $row, $ad['cost_per_purchase']); // Cost per Purchase
$sheet->setCellValue('Q' . $row, $ad['conversion_rate']); // Conversion Rate
$sheet->setCellValue('R' . $row, $ad['revenue']); // Revenue
$sheet->setCellValue('S' . $row, $ad['total_cost']); // Total Cost
$sheet->setCellValue('T' . $row, $ad['net_profit']); // Net Profit
$sheet->setCellValue('U' . $row, $ad['net_profit_margin']); // Net Profit Margin
$sheet->setCellValue('V' . $row, $ad['net_profit_on_ad_spend']); // Net Profit on Ad Spend
$row++;
}
// 填充统计信息到表格的最后一行
$statistics = $adList['statistics'];
$statisticsRow = $row; // 统计信息从当前行开始
$sheet->setCellValue('D' . $statisticsRow, 'Totals'); // 统计信息标题
$sheet->setCellValue('E' . $statisticsRow, $statistics['platform_purchase']);
$sheet->setCellValue('F' . $statisticsRow, $statistics['pixel_purchase']);
$sheet->setCellValue('G' . $statisticsRow, $statistics['roas']); // ROAS
$sheet->setCellValue('H' . $statisticsRow, $statistics['amount_spend']); // Amount Spent
$sheet->setCellValue('I' . $statisticsRow, $statistics['impressions']); // Impressions
$sheet->setCellValue('J' . $statisticsRow, $statistics['clicks']); // Clicks
$sheet->setCellValue('K' . $statisticsRow, $statistics['ctr']); // Click Through Rate
$sheet->setCellValue('L' . $statisticsRow, $statistics['adds_to_cart']); // Adds to Cart
$sheet->setCellValue('M' . $statisticsRow, $statistics['cost_per_atc']); // Cost per ATC
$sheet->setCellValue('N' . $statisticsRow, $statistics['purchases']); // Purchases
$sheet->setCellValue('O' . $statisticsRow, $statistics['purchases_value']); // Purchases Value
$sheet->setCellValue('P' . $statisticsRow, $statistics['cost_per_purchase']); // Cost per Purchase
$sheet->setCellValue('Q' . $statisticsRow, $statistics['conversion_rate']); // Conversion Rate
$sheet->setCellValue('R' . $statisticsRow, $statistics['revenue']); // Revenue
$sheet->setCellValue('S' . $statisticsRow, $statistics['total_cost']); // Total Cost
$sheet->setCellValue('T' . $statisticsRow, $statistics['net_profit']); // Net Profit
$sheet->setCellValue('U' . $statisticsRow, $statistics['net_profit_margin']); // Net Profit Margin
$sheet->setCellValue('V' . $statisticsRow, $statistics['net_profit_on_ad_spend']); // Net Profit on Ad Spend
unset($statistics);
unset($adList);
// 文件名与路径
$fileName = 'Exported_Ads_' . date('Y-m-d_H-i-s') . '.xlsx';
$filePath = public_path() . '/' . $fileName;
// 保存 Excel 文件
$writer = new Xlsx($spreadsheet);
try {
$writer->save($filePath);
return response()->download($filePath, $fileName);
} catch (\Exception $e) {
// return ['success' => false, 'message' => $e->getMessage()];
$data = [
'code' => 901,
'msg' => 'No data available for export.',
'data' => []
];
return new Response(400, ['Content-Type' => 'application/json'], json_encode($data, JSON_UNESCAPED_UNICODE));
}
}
public static function exportCreativesToExcel($platformType, $customerIds, $page, $pageSize, $keyword, $startDate = null, $endDate = null, $requireSpend = false)
{
// 获取广告创意数据
$creativeList = self::getCreativeInsightData($platformType, $customerIds, $page, $pageSize, $keyword, $startDate, $endDate, $requireSpend, false);
if (empty($creativeList['data'])) {
$data = [
'code' => 901,
'msg' => 'No data available for export.',
'data' => []
];
return new Response(400, ['Content-Type' => 'application/json'], json_encode($data, JSON_UNESCAPED_UNICODE));
}
// 创建 Spreadsheet 对象
$spreadsheet = new Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
// 设置表头
$headers = [
'A1' => 'Creative',
'B1' => 'Ad Platform',
'C1' => 'Thumbnail/Creative URL',
'D1' => 'Creative Type',
'E1' => 'Ad Count',
'F1' => 'Spend',
'G1' => 'Purchase value',
'H1' => 'ROAS',
'I1' => 'CPA',
'J1' => 'CPC (link click)',
'K1' => 'CPM',
'L1' => 'CPC (all)',
'M1' => 'AOV',
'N1' => 'Click to ATC ratio',
'O1' => 'ATC to purchase ratio',
'P1' => 'Purchases',
'Q1' => '1st frame retention',
'R1' => 'Thumbstop',
'S1' => 'CTR (outbound)',
'T1' => 'Click to purchase',
'U1' => 'CTR (all)',
'V1' => '25% video plays (rate)',
'W1' => '50% video plays (rate)',
'X1' => '75% video plays (rate)',
'Y1' => '100% video plays (rate)',
'Z1' => 'Hold rate'
];
// 填充表头
foreach ($headers as $cell => $header) {
$sheet->setCellValue($cell, $header);
}
// 填充数据
$row = 2;
foreach ($creativeList['data'] as $creative) {
$sheet->setCellValue('A' . $row, $creative['creative']); // Creative
$sheet->setCellValue('B' . $row, self::$platformMapping[$creative['platform']]); // Ad Platform
$sheet->setCellValue('C' . $row, $creative['thumbnail_url'] ?: $creative['creative_url']); // Thumbnail/Creative URL
$sheet->setCellValue('D' . $row, self::$creativeTypeMapping[$creative['creative_type']]); // Creative Type
$sheet->setCellValue('E' . $row, $creative['ad_count']); // Ad Count
$sheet->setCellValue('F' . $row, $creative['spend']); // Spend
$sheet->setCellValue('G' . $row, $creative['purchases_value']); // Purchase value
$sheet->setCellValue('H' . $row, $creative['roas']); // ROAS
$sheet->setCellValue('I' . $row, $creative['cpa']); // CPA
$sheet->setCellValue('J' . $row, $creative['cpc_link_click']); // CPC (link click)
$sheet->setCellValue('K' . $row, $creative['cpm']); // CPM
$sheet->setCellValue('L' . $row, $creative['cpc_all']); // CPC (all)
$sheet->setCellValue('M' . $row, $creative['aov']); // AOV
$sheet->setCellValue('N' . $row, $creative['click_to_atc_ratio']); // Click to ATC ratio
$sheet->setCellValue('O' . $row, $creative['atc_to_purchase_ratio']); // ATC to purchase ratio
$sheet->setCellValue('P' . $row, $creative['purchases']); // Purchases
$sheet->setCellValue('Q' . $row, $creative['first_frame_retention']); // 1st frame retention
$sheet->setCellValue('R' . $row, $creative['thumbstop']); // Thumbstop
$sheet->setCellValue('S' . $row, $creative['ctr_outbound']); // CTR (outbound)
$sheet->setCellValue('T' . $row, $creative['click_to_purchase']); // Click to purchase
$sheet->setCellValue('U' . $row, $creative['ctr_all']); // CTR (all)
$sheet->setCellValue('V' . $row, $creative['video_plays_25_rate']); // 25% video plays (rate)
$sheet->setCellValue('W' . $row, $creative['video_plays_50_rate']); // 50% video plays (rate)
$sheet->setCellValue('X' . $row, $creative['video_plays_75_rate']); // 75% video plays (rate)
$sheet->setCellValue('Y' . $row, $creative['video_plays_100_rate']); // 100% video plays (rate)
$sheet->setCellValue('Z' . $row, $creative['hold_rate']); // Hold rate
$row++;
}
// 填充统计信息到表格的最后一行
$statistics = $creativeList['statistics'];
$statisticsRow = $row; // 统计信息从当前行开始
$sheet->setCellValue('E' . $statisticsRow, 'Totals'); // 统计信息标题
$sheet->setCellValue('F' . $statisticsRow, $statistics['spend']); // Spend
$sheet->setCellValue('G' . $statisticsRow, $statistics['purchases_value']); // Purchase value
$sheet->setCellValue('H' . $statisticsRow, $statistics['roas']); // ROAS
$sheet->setCellValue('I' . $statisticsRow, $statistics['cpa']); // CPA
$sheet->setCellValue('J' . $statisticsRow, $statistics['cpc_link_click']); // CPC (link click)
$sheet->setCellValue('K' . $statisticsRow, $statistics['cpm']); // CPM
$sheet->setCellValue('L' . $statisticsRow, $statistics['cpc_all']); // CPC (all)
$sheet->setCellValue('M' . $statisticsRow, $statistics['aov']); // AOV
$sheet->setCellValue('N' . $statisticsRow, $statistics['click_to_atc_ratio']); // Click to ATC ratio
$sheet->setCellValue('O' . $statisticsRow, $statistics['atc_to_purchase_ratio']); // ATC to purchase ratio
$sheet->setCellValue('P' . $statisticsRow, $statistics['purchases']); // Purchases
$sheet->setCellValue('Q' . $statisticsRow, $statistics['first_frame_retention']); // 1st frame retention
$sheet->setCellValue('R' . $statisticsRow, $statistics['thumbstop']); // Thumbstop
$sheet->setCellValue('S' . $statisticsRow, $statistics['ctr_outbound']); // CTR (outbound)
$sheet->setCellValue('T' . $statisticsRow, $statistics['click_to_purchase']); // Click to purchase
$sheet->setCellValue('U' . $statisticsRow, $statistics['ctr_all']); // CTR (all)
$sheet->setCellValue('V' . $statisticsRow, $statistics['video_plays_25_rate']); // 25% video plays (rate)
$sheet->setCellValue('W' . $statisticsRow, $statistics['video_plays_50_rate']); // 50% video plays (rate)
$sheet->setCellValue('X' . $statisticsRow, $statistics['video_plays_75_rate']); // 75% video plays (rate)
$sheet->setCellValue('Y' . $statisticsRow, $statistics['video_plays_100_rate']); // 100% video plays (rate)
$sheet->setCellValue('Z' . $statisticsRow, $statistics['hold_rate']); // Hold rate
// 释放内存
unset($statistics);
unset($creativeList);
// 文件名与路径
$fileName = 'Exported_Creatives_' . date('Y-m-d_H-i-s') . '.xlsx';
$filePath = public_path() . '/' . $fileName;
// 保存 Excel 文件
$writer = new Xlsx($spreadsheet);
try {
$writer->save($filePath);
return response()->download($filePath, $fileName);
} catch (\Exception $e) {
$data = [
'code' => 901,
'msg' => 'No data available for export.',
'data' => []
];
return new Response(400, ['Content-Type' => 'application/json'], json_encode($data, JSON_UNESCAPED_UNICODE));
}
}
/**
* 获取广告组列表
*/
public static function getAdsetList($platformType, $customerIds, $page, $pageSize, $keyword, $startDate = null, $endDate = null, $status = 0, $requireSpend = false, $countOnly = false)
{
// 检查 customerIds 是否为空,直接返回空结构
if (empty($customerIds)) {
return $countOnly ? 0 : [
'pagination' => [
'startIndex' => 0,
'maxResults' => 0,
'count' => 0,
'pageNo' => $page,
'pageSize' => $pageSize,
'pages' => 0,
],
'statistics' => [],
'data' => [],
];
}
// 基础查询:广告活动和日数据表联接
$query = BpsAdSet::alias('s')
->cache(false) // 强制不使用缓存
->where('s.account_id', 'in', $customerIds);
// 如果只需要记录条数,执行计数查询
if ($countOnly) {
return $query->count();
}
// 动态构建日期条件
$dateCondition = '';
if ($startDate && $endDate) {
$dateCondition = "d.date BETWEEN '{$startDate}' AND '{$endDate}'";
}
// 基础查询:广告组和日数据表联接
$query->leftJoin('bps.bps_ads_insights d', "s.ad_set_id = d.ad_set_id AND s.platform_type = d.platform AND {$dateCondition}")
->leftJoin('bps.bps_ads_campaign c', 's.campaign_id = c.campaign_id') // 联接广告系列表
->field('s.ad_set_id, s.status as status, s.name, s.account_id,s.platform_type,c.name as campaign_name,
COALESCE(SUM(d.clicks), 0) as clicks,
COALESCE(SUM(d.spend) / 1000000, 0) as spend,
COALESCE(SUM(d.impressions), 0) as impressions,
COALESCE(SUM(d.adds_to_cart), 0) as adds_to_cart,
COALESCE(SUM(d.purchases), 0) as purchases,
COALESCE(SUM(d.platform_purchase), 0) as platform_purchase,
COALESCE(SUM(d.pixel_purchase), 0) as pixel_purchase,
COALESCE(SUM(d.purchases_value / 1000000), 0) as purchases_value,
COALESCE(SUM(d.platform_purchase_value / 1000000), 0) as revenue,
COALESCE(SUM(d.total_cost / 1000000), 0) as total_cost,
-1 as conversion_rate, -1 as roas, -1 as ctr,-1 as net_profit,-1 as net_profit_margin,-1 as net_profit_on_ad_spend')
->group('s.ad_set_id, s.status, s.account_id, s.name,s.platform_type,c.name');
// ->where('s.account_id', 'in', $customerIds); // 添加 customerIds 条件
// 根据 $requireSpend 参数决定是否添加 SUM(d.spend) > 0 的条件
if ($requireSpend) {
$query->having('SUM(d.spend) > 0');
}
if ($status !== 0) {
$query->where('s.status', '=', $status);
}
// 添加关键字过滤条件
$query->where(function ($query) use ($keyword, $platformType) {
if ($keyword) {
// $query->where('s.name', 'like', '%' . $keyword . '%');
$query->whereRaw('LOWER(s.name) LIKE ?', ['%' . strtolower($keyword) . '%']);
}
if ($platformType) {
$platformType = (int)$platformType;
$query->where('s.platform_type', '=', $platformType);
}
});
$query->order('s.platform_type, s.account_id, s.status', 'asc');
// 获取所有符合条件的数据(不分页)
$allAdsets = $query->select()->toArray();
$total_spend = array_sum(array_column($allAdsets, 'spend'));
$total_cost = array_sum(array_column($allAdsets, 'total_cost'));
$total_impressions = array_sum(array_column($allAdsets, 'impressions'));
$total_clicks = array_sum(array_column($allAdsets, 'clicks'));
$total_purchases_value = array_sum(array_column($allAdsets, 'purchases_value'));
$total_revenue = array_sum(array_column($allAdsets, 'revenue'));
$total_purchases = array_sum(array_column($allAdsets, 'purchases'));
// $total_platform_purchase = array_sum(array_column($allAdsets, 'platform_purchase'));
// $total_pixel_purchase = array_sum(array_column($allAdsets, 'pixel_purchase'));
$adds_to_cart = array_sum(array_column($allAdsets, 'adds_to_cart'));
$cost_per_purchase = $total_purchases == 0 ? 0 : round($total_spend / $total_purchases, 2);
// 汇总统计数据
$statistics = [
'platform_purchase' => number_format(array_sum(array_column($allAdsets, 'platform_purchase'))),
'pixel_purchase' => number_format(array_sum(array_column($allAdsets, 'pixel_purchase'))),
'roas' => $total_spend == 0 ? '-' : round($total_revenue / $total_spend, 2) . 'x',
'amount_spend' => '$' . number_format($total_spend, 2) ?: '$0.00', // 格式化支出
'clicks' => number_format($total_clicks),
'impressions' => number_format($total_impressions),
'adds_to_cart' => number_format($adds_to_cart),
'cost_per_atc' => $adds_to_cart == 0 ? '-' : '$' . number_format(($total_spend / $adds_to_cart), 2),
'purchases' => number_format($total_purchases),
'purchases_value' => array_sum(array_column($allAdsets, 'purchases_value')),
'cost_per_purchase' => '$' . number_format($cost_per_purchase, 2) ?: '$0.00',
'revenue' => '$' . number_format(array_sum(array_column($allAdsets, 'revenue')), 2) ?: '$0.00', // 格式化收入
'total_cost' => '$' . number_format($total_cost, 2) ?: '$0.00', // 格式化总成本
'conversion_rate' => $total_clicks == 0 ? '-' : round(($total_purchases / $total_clicks) * 100, 2) . '%', // 转换率
'net_profit' => ($total_revenue - $total_cost) >= 0 ? '+$' . number_format($total_revenue - $total_cost, 2) : '-$' . number_format(abs($total_revenue - $total_cost), 2), // 净利润
'net_profit_margin' => $total_revenue == 0 ? '-' : round(($total_revenue - $total_cost) / $total_revenue, 2) * 100 . '%', // 净利润率
'net_profit_on_ad_spend' => $total_spend == 0 ? '-' : round(($total_revenue - $total_cost) / $total_spend, 2) * 100 . '%', // 广告支出净利润
// 计算总的 CTR
'ctr' => ($total_impressions > 0) ? number_format(($total_clicks / $total_impressions) * 100, 2) . '%' : '-', // 格式化为百分比
];
// 获取分页数据
// $adsets = $query->paginate($pageSize, false, ['page' => $page]);
$page = max(1, (int)$page); // 确保页码不小于 1
$adsets = $query->limit(($page - 1) * $pageSize, $pageSize)->select();
// 确保数据格式统一
$result = array_map(function ($item) {
// CTR 的计算:点击率 = 点击数 / 展示数
$ctr = $item['impressions'] > 0 ? number_format(($item['clicks'] / $item['impressions']) * 100, 2) . '%' : '-';
// Conversion Rate 的计算:转换率 = 购买数 / 点击数
$conversion_rate = $item['clicks'] > 0 ? round(($item['purchases'] / $item['clicks']) * 100, 2) . '%' : '-';
// Net Profit 的计算:净利润 = 收入 - 总成本
$net_profit = ($item['revenue'] - $item['total_cost']) >= 0 ? '+$' . number_format($item['revenue'] - $item['total_cost'], 2) : '-$' . number_format(abs($item['revenue'] - $item['total_cost']), 2);
// Net Profit Margin 的计算:净利润率 = 净利润 / 收入
$net_profit_margin = $item['revenue'] > 0 ? round(($item['revenue'] - $item['total_cost']) / $item['revenue'], 2) * 100 . '%' : '-';
// Net Profit on Ad Spend 的计算:广告支出净利润 = (收入 - 总成本) / 广告支出
$net_profit_on_ad_spend = $item['spend'] > 0 ? round(($item['revenue'] - $item['total_cost']) / $item['spend'], 2) * 100 . '%' : '-';
return [
'id' => $item['ad_set_id'],
'platform_type' => $item['platform_type'],
'account_id' => $item['account_id'], // 映射为 customer_id
'name' => $item['name'] ?: '-', // 若 name 为空则显示 '-'
'campaign_name' => $item['campaign_name'] ?: '-', // 若 name 为空则显示 '-'
'status' => $item['status'],
'platform_purchase' => number_format($item['platform_purchase']),
'pixel_purchase' => number_format($item['pixel_purchase']),
'roas' => $item['spend'] == 0 ? '-' : round($item['revenue'] / $item['spend'], 2) . 'x',
'spend' => '$' . number_format($item['spend'], 2), // 格式化支出
'impressions' => number_format($item['impressions']),
'clicks' => number_format($item['clicks']),
'ctr' => $ctr, // CTR 字段
'adds_to_cart' => number_format($item['adds_to_cart']),
'cost_per_atc' => $item['adds_to_cart'] > 0 ? '$' . number_format(($item['spend'] / $item['adds_to_cart']), 2) : '-',
'purchases' => number_format($item['purchases']),
'purchases_value' => '$' . number_format($item['purchases_value'], 2), // 格式化购买金额
'cost_per_purchase' => $item['purchases'] > 0 ? '$' . number_format(($item['spend'] / $item['purchases']), 2) : '-',
'revenue' => '$' . number_format($item['revenue'], 2), // 格式化收入
'total_cost' => '$' . number_format($item['total_cost'], 2), // 格式化总成本
'conversion_rate' => $conversion_rate, // 没有提供有效的计算,保持为 '-'
'net_profit' => $net_profit, // 没有提供 net_profit 计算,保持为 '-'
'net_profit_margin' => $net_profit_margin, // 没有提供 net_profit_margin 计算,保持为 '-'
'net_profit_on_ad_spend' => $net_profit_on_ad_spend, // 没有提供 net_profit_on_ad_spend 计算,保持为 '-'
];
}, $adsets->toArray());
// Pagination 数据
$pagination = [
'startIndex' => ($page - 1) * $pageSize + 1, // 确保索引从 1 开始
'maxResults' => count($adsets), // 当前页实际返回数据数量
'count' => count($allAdsets), // 符合条件的总记录数
'pageNo' => $page, // 当前页码
'pageSize' => $pageSize, // 每页条数
'pages' => (int)ceil(count($allAdsets) / $pageSize), // 总页数
];
return [
'pagination' => $pagination,
'statistics' => $statistics,
'data' => $result,
];
}
/**
* 导出广告账号列表到 Excel
*
* @param int $platformType 平台类型
* @param array $customerIds 客户ID列表
* @param int $page 当前页码
* @param int $pageSize 每页条数
* @param string $keyword 关键字
* @param string|null $startDate 开始日期
* @param string|null $endDate 结束日期
* @param int $status 广告系列状态
*/
public static function exportAccountsToExcel($platformType, $merchantId, $storeId, $customerIds, $page, $pageSize, $keyword, $startDate = null, $endDate = null, $requireSpend = false)
{
// 调用 getCampaignList 获取广告系列数据
$accountList = self::getAccountList($platformType, $merchantId, $storeId, $customerIds, $page, $pageSize, $keyword, $startDate, $endDate, $requireSpend);
if (empty($accountList['data'])) {
$data = [
'code' => 901,
'msg' => 'No data available for export.',
'data' => []
];
return new Response(400, ['Content-Type' => 'application/json'], json_encode($data, JSON_UNESCAPED_UNICODE));
}
// 创建 Spreadsheet 对象
$spreadsheet = new Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
// 设置表头
$headers = [
'A1' => 'Ad Account Id',
'B1' => 'Advertiser Name',
'C1' => 'Ad Platform',
'D1' => 'Platform Purchases',
'E1' => 'Best Pixel Purchases',
'F1' => 'ROAS',
'G1' => 'Amount Spent',
'H1' => 'Impressions',
'I1' => 'Clicks',
'J1' => 'Click Through Rate',
'K1' => 'Adds to Cart',
'L1' => 'Cost per ATC',
'M1' => 'Purchases',
'N1' => 'Purchases Value',
'O1' => 'Cost per Purchase',
'P1' => 'Conversion Rate',
'Q1' => 'Revenue',
'R1' => 'Total Cost',
'S1' => 'Net Profit',
'T1' => 'Net Profit Margin',
'U1' => 'Net Profit on Ad Spend'
];
foreach ($headers as $cell => $header) {
$sheet->setCellValue($cell, $header);
}
// 填充数据
// 填充数据
$row = 2;
foreach ($accountList['data'] as $account) {
$sheet->setCellValueExplicit('A' . $row, (string)$account['user_id'], \PhpOffice\PhpSpreadsheet\Cell\DataType::TYPE_STRING); // Ad Account Id
$sheet->setCellValue('B' . $row, $account['advertiser_name']); // Advertiser Name
$sheet->setCellValue('C' . $row, self::$platformMapping[$account['platform_type']]); // Ad Platform
$sheet->setCellValue('D' . $row, $account['platform_purchase']);
$sheet->setCellValue('E' . $row, $account['pixel_purchase']);
$sheet->setCellValue('F' . $row, $account['roas']); // ROAS原 G 列)
$sheet->setCellValue('G' . $row, $account['spend']); // Amount Spent原 H 列)
$sheet->setCellValue('H' . $row, $account['impressions']); // Impressions原 I 列)
$sheet->setCellValue('I' . $row, $account['clicks']); // Clicks原 J 列)
$sheet->setCellValue('J' . $row, $account['ctr']); // Click Through Rate原 K 列)
$sheet->setCellValue('K' . $row, $account['adds_to_cart']); // Adds to Cart原 L 列)
$sheet->setCellValue('L' . $row, $account['cost_per_atc']); // Cost per ATC原 M 列)
$sheet->setCellValue('M' . $row, $account['purchases']); // Purchases原 N 列)
$sheet->setCellValue('N' . $row, $account['purchases_value']); // Purchases Value原 O 列)
$sheet->setCellValue('O' . $row, $account['cost_per_purchase']); // Cost per Purchase原 P 列)
$sheet->setCellValue('P' . $row, $account['conversion_rate']); // Conversion Rate原 Q 列)
$sheet->setCellValue('Q' . $row, $account['revenue']); // Revenue原 R 列)
$sheet->setCellValue('R' . $row, $account['total_cost']); // Total Cost原 S 列)
$sheet->setCellValue('S' . $row, $account['net_profit']); // Net Profit原 T 列)
$sheet->setCellValue('T' . $row, $account['net_profit_margin']); // Net Profit Margin原 U 列)
$sheet->setCellValue('U' . $row, $account['net_profit_on_ad_spend']); // Net Profit on Ad Spend原 V 列)
$row++;
}
// 填充统计信息到表格的最后一行
$statistics = $accountList['statistics'];
$statisticsRow = $row; // 统计信息从当前行开始
$sheet->setCellValue('C' . $statisticsRow, 'Totals'); // 统计信息标题(调整到 A 列)
$sheet->setCellValue('D' . $statisticsRow, $statistics['platform_purchase']);
$sheet->setCellValue('E' . $statisticsRow, $statistics['pixel_purchase']);
$sheet->setCellValue('F' . $statisticsRow, $statistics['roas']); // ROAS原 G 列)
$sheet->setCellValue('G' . $statisticsRow, $statistics['amount_spend']); // Amount Spent原 H 列)
$sheet->setCellValue('H' . $statisticsRow, $statistics['impressions']); // Impressions原 I 列)
$sheet->setCellValue('I' . $statisticsRow, $statistics['clicks']); // Clicks原 J 列)
$sheet->setCellValue('J' . $statisticsRow, $statistics['ctr']); // Click Through Rate原 K 列)
$sheet->setCellValue('K' . $statisticsRow, $statistics['adds_to_cart']); // Adds to Cart原 L 列)
$sheet->setCellValue('L' . $statisticsRow, $statistics['cost_per_atc']); // Cost per ATC原 M 列)
$sheet->setCellValue('M' . $statisticsRow, $statistics['purchases']); // Purchases原 N 列)
$sheet->setCellValue('N' . $statisticsRow, $statistics['purchases_value']); // Purchases Value原 O 列)
$sheet->setCellValue('O' . $statisticsRow, $statistics['cost_per_purchase']); // Cost per Purchase原 P 列)
$sheet->setCellValue('P' . $statisticsRow, $statistics['conversion_rate']); // Conversion Rate原 Q 列)
$sheet->setCellValue('Q' . $statisticsRow, $statistics['revenue']); // Revenue原 R 列)
$sheet->setCellValue('R' . $statisticsRow, $statistics['total_cost']); // Total Cost原 S 列)
$sheet->setCellValue('S' . $statisticsRow, $statistics['net_profit']); // Net Profit原 T 列)
$sheet->setCellValue('T' . $statisticsRow, $statistics['net_profit_margin']); // Net Profit Margin原 U 列)
$sheet->setCellValue('U' . $statisticsRow, $statistics['net_profit_on_ad_spend']); // Net Profit on Ad Spend原 V 列)
unset($statistics);
unset($accountList);
// 文件名与路径
$fileName = 'Exported_Accounts_' . date('Y-m-d_H-i-s') . '.xlsx';
$filePath = public_path() . '/' . $fileName;
// 保存 Excel 文件
$writer = new Xlsx($spreadsheet);
try {
$writer->save($filePath);
return response()->download($filePath, $fileName);
} catch (\Exception $e) {
// return ['success' => false, 'message' => $e->getMessage()];
$data = [
'code' => 901,
'msg' => 'No data available for export.',
'data' => []
];
return new Response(400, ['Content-Type' => 'application/json'], json_encode($data, JSON_UNESCAPED_UNICODE));
}
}
public static function getAccountList($platformType, $merchantId, $storeId, $customerIds, $page, $pageSize, $keyword, $startDate = null, $endDate = null, $requireSpend = false, $countOnly = false)
{
// 检查 customerIds 是否为空,直接返回计数为 0
if (empty($customerIds)) {
return $countOnly ? 0 : [
'pagination' => [
'startIndex' => 0,
'maxResults' => $pageSize,
'count' => 0,
'pageNo' => $page,
'pageSize' => $pageSize,
'pages' => 0,
],
'statistics' => [],
'data' => [],
];
}
// 构建查询条件
// $query = ThirdUserAdvertiser::alias('a')
// ->cache(false)
// ->where('a.advertiser_id', 'in', $customerIds);
$query = BpsAdsMerchantStoreRelation::alias('bamr')
->cache(false)
->where('bamr.merchant_id', '=', $merchantId)
->where('bamr.store_id', '=', $storeId)
->where('bamr.account_id', 'in', $customerIds);
// 仅计数时优化查询
if ($countOnly) {
return $query->count('distinct(bamr.account_id)'); // 只查询总记录数
}
// 动态构建日期条件
$dateCondition = '';
if ($startDate && $endDate) {
$dateCondition = "d.date BETWEEN '{$startDate}' AND '{$endDate}'";
}
// 基础查询:广告和日数据表联接
// 其他联表及字段计算
$query->leftJoin('bps.bps_ads_insights d', "bamr.account_id = d.account_id AND {$dateCondition}")
->field('bamr.account_id, bamr.account_name,bamr.platform,
COALESCE(SUM(d.clicks), 0) as clicks,
COALESCE(SUM(d.spend) / 1000000, 0) as spend,
COALESCE(SUM(d.impressions), 0) as impressions,
COALESCE(SUM(d.adds_to_cart), 0) as adds_to_cart,
COALESCE(SUM(d.purchases), 0) as purchases,
COALESCE(SUM(d.platform_purchase), 0) as platform_purchase,
COALESCE(SUM(d.pixel_purchase), 0) as pixel_purchase,
COALESCE(SUM(d.purchases_value) / 1000000, 0) as purchases_value,
COALESCE(SUM(d.platform_purchase_value) / 1000000, 0) as revenue,
COALESCE(SUM(d.total_cost) / 1000000, 0) as total_cost,
-1 as conversion_rate, -1 as roas, -1 as ctr, -1 as net_profit, -1 as net_profit_margin, -1 as net_profit_on_ad_spend')
->group('bamr.account_id, bamr.account_name,bamr.platform');
// ->where('bamr.account_id', 'in', $customerIds); // 添加 customerIds 条件
// 如果传入了 status 参数,按状态筛选
// if ($status !== 0) {
// $query->where('a.status', '=', $status);
// }
// 添加关键字过滤条件
$query->where(function ($query) use ($keyword, $platformType) {
if ($keyword) {
$query->whereRaw('LOWER(bamr.account_name) LIKE ?', ['%' . strtolower($keyword) . '%']);
}
if ($platformType) {
// $a = (int)$platformType;
// $platformTypeNames = [1 => 'facebook', 2 => 'google', 3 => 'tiktok'];
$query->where('bamr.platform', '=', $platformType);
}
});
$query->order('bamr.platform', 'asc');
// 根据 $requireSpend 参数决定是否添加 SUM(d.spend) > 0 的条件
if ($requireSpend) {
$query->having('SUM(d.spend) > 0');
}
// 获取所有符合条件的数据(不分页)
$allAccounts = $query->select()->toArray();
// 打印调试 SQL 查询
// $sql = $query->getLastSql();
// dump($sql);
// 汇总统计数据
$total_spend = array_sum(array_column($allAccounts, 'spend'));
$total_cost = array_sum(array_column($allAccounts, 'total_cost'));
$total_impressions = array_sum(array_column($allAccounts, 'impressions'));
$total_clicks = array_sum(array_column($allAccounts, 'clicks'));
$total_purchases_value = array_sum(array_column($allAccounts, 'purchases_value'));
$total_revenue = array_sum(array_column($allAccounts, 'revenue'));
$total_purchases = array_sum(array_column($allAccounts, 'purchases'));
$adds_to_cart = array_sum(array_column($allAccounts, 'adds_to_cart'));
$cost_per_purchase = $total_purchases == 0 ? 0 : round($total_spend / $total_purchases, 2);
$statistics = [
'platform_purchase' => number_format(array_sum(array_column($allAccounts, 'platform_purchase'))),
'pixel_purchase' => number_format(array_sum(array_column($allAccounts, 'pixel_purchase'))),
'roas' => $total_spend == 0 ? '-' : round($total_revenue / $total_spend, 2) . 'x',
'amount_spend' => '$' . number_format($total_spend, 2) ?: '$0.00', // 格式化支出
'clicks' => number_format($total_clicks),
'impressions' => number_format($total_impressions),
'adds_to_cart' => number_format($adds_to_cart),
'cost_per_atc' => $adds_to_cart == 0 ? '-' : '$' . number_format(($total_spend / $adds_to_cart), 2),
'purchases' => number_format($total_purchases),
'purchases_value' => '$' . number_format($total_purchases_value, 2) ?: '$0.00', // 格式化销售额$total_purchases_value,,
'cost_per_purchase' => '$' . number_format($cost_per_purchase, 2) ?: '$0.00',
'revenue' => '$' . number_format(array_sum(array_column($allAccounts, 'revenue')), 2) ?: '$0.00', // 格式化收入
'total_cost' => '$' . number_format($total_cost, 2) ?: '$0.00', // 格式化总成本
'conversion_rate' => $total_clicks == 0 ? '-' : round(($total_purchases / $total_clicks) * 100, 2) . '%', // 转换率
'net_profit' => ($total_revenue - $total_cost) >= 0 ? '+$' . number_format($total_revenue - $total_cost, 2) : '-$' . number_format(abs($total_revenue - $total_cost), 2), // 净利润
'net_profit_margin' => $total_revenue == 0 ? '-' : round(($total_revenue - $total_cost) / $total_revenue, 2) * 100 . '%', // 净利润率
'net_profit_on_ad_spend' => $total_spend == 0 ? '-' : round(($total_revenue - $total_cost) / $total_spend, 2) * 100 . '%', // 平均广告花费的净利润
'ctr' => ($total_impressions > 0) ? number_format(($total_clicks / $total_impressions) * 100, 2) . '%' : '-', // 格式化为百分比
];
// 获取分页数据
$page = max(1, (int)$page); // 确保页码不小于 1
// $ads = $query->page($page, $pageSize)->select();
$accounts = $query->limit(($page - 1) * $pageSize, $pageSize)->select();
// dump($ads);
// 确保数据格式统一
$result = array_map(function ($item) {
// CTR 的计算:点击率 = 点击数 / 展示数
$ctr = $item['impressions'] > 0 ? number_format(($item['clicks'] / $item['impressions']) * 100, 2) . '%' : '-';
// Conversion Rate 的计算:转换率 = 购买数 / 点击数
$conversion_rate = $item['clicks'] > 0 ? round(($item['purchases'] / $item['clicks']) * 100, 2) . '%' : '-';
// Net Profit 的计算:净利润 = 收入 - 总成本
$net_profit = ($item['revenue'] - $item['total_cost']) >= 0 ? '+$' . number_format($item['revenue'] - $item['total_cost'], 2) : '-$' . number_format(abs($item['revenue'] - $item['total_cost']), 2);
// Net Profit Margin 的计算:净利润率 = 净利润 / 收入
$net_profit_margin = $item['revenue'] > 0 ? round(($item['revenue'] - $item['total_cost']) / $item['revenue'], 2) * 100 . '%' : '-';
// Net Profit on Ad Spend 的计算:广告支出净利润 = (收入 - 总成本) / 广告支出
$net_profit_on_ad_spend = $item['spend'] > 0 ? round(($item['revenue'] - $item['total_cost']) / $item['spend'], 2) * 100 . '%' : '-';
// $platformTypeIds = ['facebook' => 1, 'google' => 2, 'tiktok' => 3];
return [
'user_id' => $item['account_id'],
// 'platform_type' => $platformTypeIds[$item['third_type']],
'platform_type' => $item['platform'],
'advertiser_name' => $item['account_name'] ?: '-', // 若 name 为空则显示 '-'
// 'ad_set_name' => $item['ad_set_name'] ?: '-', // 若 name 为空则显示 '-'
// 'status' => $item['status'],
'platform_purchase' => number_format($item['platform_purchase']),
'pixel_purchase' => number_format($item['pixel_purchase']),
'roas' => $item['spend'] == 0 ? '-' : round($item['revenue'] / $item['spend'], 2) . 'x',
'spend' => '$' . number_format($item['spend'], 2), // 格式化支出
'amount_spend' => '$' . number_format($item['spend'], 2), // 格式化支出
'impressions' => number_format($item['impressions']),
'clicks' => number_format($item['clicks']),
'ctr' => $ctr, // CTR 字段
'adds_to_cart' => number_format($item['adds_to_cart']),
'cost_per_atc' => $item['adds_to_cart'] > 0 ? '$' . number_format(($item['spend'] / $item['adds_to_cart']), 2) : '-',
'purchases' => number_format($item['purchases']),
'purchases_value' => '$' . number_format($item['purchases_value'], 2), // 格式化购买金额
'cost_per_purchase' => $item['purchases'] > 0 ? '$' . number_format(($item['spend'] / $item['purchases']), 2) : '-',
'revenue' => '$' . number_format($item['revenue'], 2), // 格式化收入
'total_cost' => '$' . number_format($item['total_cost'], 2), // 格式化总成本
'conversion_rate' => $conversion_rate,
'net_profit' => $net_profit,
'net_profit_margin' => $net_profit_margin,
'net_profit_on_ad_spend' => $net_profit_on_ad_spend,
];
}, $accounts->toArray());
// Pagination 数据
$pagination = [
'startIndex' => ($page - 1) * $pageSize + 1, // 确保索引从 1 开始
'maxResults' => count($accounts), // 当前页实际返回数据数量
'count' => count($allAccounts), // 符合条件的总记录数
'pageNo' => $page, // 当前页码
'pageSize' => $pageSize, // 每页条数
'pages' => (int)ceil(count($allAccounts) / $pageSize), // 总页数
];
return [
'pagination' => $pagination,
'statistics' => $statistics,
'data' => $result,
];
}
public static function getAdList($platformType, $customerIds, $page, $pageSize, $keyword, $startDate = null, $endDate = null, $status = 0, $requireSpend = false, $countOnly = false)
{
// 检查 customerIds 是否为空,直接返回空结构
if (empty($customerIds)) {
return $countOnly ? 0 : [
'pagination' => [
'startIndex' => 0,
'maxResults' => 0,
'count' => 0,
'pageNo' => $page,
'pageSize' => $pageSize,
'pages' => 0,
],
'statistics' => [],
'data' => [],
];
}
// 基础查询:广告活动和日数据表联接
$query = BpsAdAd::alias('a')
->cache(false) // 强制不使用缓存
->where('a.account_id', 'in', $customerIds);
// 如果只需要记录条数,执行计数查询
if ($countOnly) {
return $query->count();
}
// 动态构建日期条件
$dateCondition = '';
if ($startDate && $endDate) {
$dateCondition = "d.date BETWEEN '{$startDate}' AND '{$endDate}'";
}
// 基础查询:广告和日数据表联接
$query->leftJoin('bps.bps_ads_insights d', "a.ad_id = d.ad_id AND a.platform_type = d.platform AND {$dateCondition}")
->leftJoin('bps.bps_ads_set s', 'a.ad_set_id = s.ad_set_id') // 联接广告组表,获取 ad_set_name
->field('a.ad_id, a.status as status, a.name, a.account_id,a.platform_type,s.name as ad_set_name,
COALESCE(SUM(d.clicks), 0) as clicks,
COALESCE(SUM(d.spend) / 1000000, 0) as spend,
COALESCE(SUM(d.impressions), 0) as impressions,
COALESCE(SUM(d.adds_to_cart), 0) as adds_to_cart,
COALESCE(SUM(d.purchases), 0) as purchases,
COALESCE(SUM(d.platform_purchase), 0) as platform_purchase,
COALESCE(SUM(d.pixel_purchase), 0) as pixel_purchase,
COALESCE(SUM(d.purchases_value) / 1000000, 0) as purchases_value,
COALESCE(SUM(d.platform_purchase_value / 1000000), 0) as revenue,
COALESCE(SUM(d.total_cost) / 1000000, 0) as total_cost,
-1 as conversion_rate, -1 as roas, -1 as ctr, -1 as net_profit, -1 as net_profit_margin, -1 as net_profit_on_ad_spend')
->group('a.ad_id, a.status, a.account_id, a.name,a.platform_type, s.name');
// ->where('a.account_id', 'in', $customerIds); // 添加 customerIds 条件
// 如果传入了 status 参数,按状态筛选
if ($status !== 0) {
$query->where('a.status', '=', $status);
}
// 根据 $requireSpend 参数决定是否添加 SUM(d.spend) > 0 的条件
if ($requireSpend) {
$query->having('SUM(d.spend) > 0');
}
// 添加关键字过滤条件
$query->where(function ($query) use ($keyword, $platformType) {
if ($keyword) {
// $query->where('a.name', 'like', '%' . $keyword . '%');
$query->whereRaw('LOWER(a.name) LIKE ?', ['%' . strtolower($keyword) . '%']);
}
if ($platformType) {
$platformType = (int)$platformType;
$query->where('a.platform_type', '=', $platformType);
}
});
$query->order('a.platform_type, a.account_id, a.status', 'asc');
// 获取所有符合条件的数据(不分页)
$allAds = $query->select()->toArray();
// 汇总统计数据
$total_spend = array_sum(array_column($allAds, 'spend'));
$total_cost = array_sum(array_column($allAds, 'total_cost'));
$total_impressions = array_sum(array_column($allAds, 'impressions'));
$total_clicks = array_sum(array_column($allAds, 'clicks'));
$total_purchases_value = array_sum(array_column($allAds, 'purchases_value'));
$total_revenue = array_sum(array_column($allAds, 'revenue'));
$total_purchases = array_sum(array_column($allAds, 'purchases'));
$adds_to_cart = array_sum(array_column($allAds, 'adds_to_cart'));
$cost_per_purchase = $total_purchases == 0 ? 0 : round($total_spend / $total_purchases, 2);
// 生成唯一缓存键
$cacheKey = 'adlist_stats:' . md5(serialize([
$platformType,
$customerIds,
$pageSize,
$keyword,
$startDate,
$endDate,
$status,
$requireSpend
]));
// 尝试从缓存读取统计数据
if (!$countOnly && $cached = Redis::get($cacheKey)) {
$statistics = json_decode($cached, true);
// 跳过后续统计计算直接使用缓存数据...
} else {
$statistics = [
'platform_purchase' => number_format(array_sum(array_column($allAds, 'platform_purchase'))),
'pixel_purchase' => number_format(array_sum(array_column($allAds, 'pixel_purchase'))),
'roas' => $total_spend == 0 ? '-' : round($total_revenue / $total_spend, 2) . 'x',
'amount_spend' => '$' . number_format($total_spend, 2) ?: '$0.00', // 格式化支出
'clicks' => number_format($total_clicks),
'impressions' => number_format($total_impressions),
'adds_to_cart' => number_format($adds_to_cart),
'cost_per_atc' => $adds_to_cart == 0 ? '-' : '$' . number_format(($total_spend / $adds_to_cart), 2),
'purchases' => number_format($total_purchases),
'purchases_value' => array_sum(array_column($allAds, 'purchases_value')),
'cost_per_purchase' => '$' . number_format($cost_per_purchase, 2) ?: '$0.00',
'revenue' => '$' . number_format(array_sum(array_column($allAds, 'revenue')), 2) ?: '$0.00', // 格式化收入
'total_cost' => '$' . number_format($total_cost, 2) ?: '$0.00', // 格式化总成本
'conversion_rate' => $total_clicks == 0 ? '-' : round(($total_purchases / $total_clicks) * 100, 2) . '%', // 转换率
'net_profit' => ($total_revenue - $total_cost) >= 0 ? '+$' . number_format($total_revenue - $total_cost, 2) : '-$' . number_format(abs($total_revenue - $total_cost), 2), // 净利润
'net_profit_margin' => $total_revenue == 0 ? '-' : round(($total_revenue - $total_cost) / $total_revenue, 2) * 100 . '%', // 净利润率
'net_profit_on_ad_spend' => $total_spend == 0 ? '-' : round(($total_revenue - $total_cost) / $total_spend, 2) * 100 . '%', // 平均广告花费的净利润
'ctr' => ($total_impressions > 0) ? number_format(($total_clicks / $total_impressions) * 100, 2) . '%' : '-', // 格式化为百分比
];
// 将新生成的统计数据存入缓存10分钟有效期
Redis::setex($cacheKey, 600, json_encode($statistics));
}
// 获取分页数据
$page = max(1, (int)$page); // 确保页码不小于 1
// $ads = $query->page($page, $pageSize)->select();
$ads = $query->limit(($page - 1) * $pageSize, $pageSize)->select();
// dump($ads);
// 确保数据格式统一
$result = array_map(function ($item) {
// CTR 的计算:点击率 = 点击数 / 展示数
$ctr = $item['impressions'] > 0 ? number_format(($item['clicks'] / $item['impressions']) * 100, 2) . '%' : '-';
// Conversion Rate 的计算:转换率 = 购买数 / 点击数
$conversion_rate = $item['clicks'] > 0 ? round(($item['purchases'] / $item['clicks']) * 100, 2) . '%' : '-';
// Net Profit 的计算:净利润 = 收入 - 总成本
$net_profit = ($item['revenue'] - $item['total_cost']) >= 0 ? '+$' . number_format($item['revenue'] - $item['total_cost'], 2) : '-$' . number_format(abs($item['revenue'] - $item['total_cost']), 2);
// Net Profit Margin 的计算:净利润率 = 净利润 / 收入
$net_profit_margin = $item['revenue'] > 0 ? round(($item['revenue'] - $item['total_cost']) / $item['revenue'], 2) * 100 . '%' : '-';
// Net Profit on Ad Spend 的计算:广告支出净利润 = (收入 - 总成本) / 广告支出
$net_profit_on_ad_spend = $item['spend'] > 0 ? round(($item['revenue'] - $item['total_cost']) / $item['spend'], 2) * 100 . '%' : '-';
return [
'id' => $item['ad_id'],
'platform_type' => $item['platform_type'],
'account_id' => $item['account_id'], // 映射为 customer_id
'name' => $item['name'] ?: '-', // 若 name 为空则显示 '-'
'ad_set_name' => $item['ad_set_name'] ?: '-', // 若 name 为空则显示 '-'
'status' => $item['status'],
'platform_purchase' => number_format($item['platform_purchase']),
'pixel_purchase' => number_format($item['pixel_purchase']),
'roas' => $item['spend'] == 0 ? '-' : round($item['revenue'] / $item['spend'], 2) . 'x',
'spend' => '$' . number_format($item['spend'], 2), // 格式化支出
'impressions' => number_format($item['impressions']),
'clicks' => number_format($item['clicks']),
'ctr' => $ctr, // CTR 字段
'adds_to_cart' => number_format($item['adds_to_cart']),
'cost_per_atc' => $item['adds_to_cart'] > 0 ? '$' . number_format(($item['spend'] / $item['adds_to_cart']), 2) : '-',
'purchases' => number_format($item['purchases']),
'purchases_value' => '$' . number_format($item['purchases_value'], 2), // 格式化购买金额
'cost_per_purchase' => $item['purchases'] > 0 ? '$' . number_format(($item['spend'] / $item['purchases']), 2) : '-',
'revenue' => '$' . number_format($item['revenue'], 2), // 格式化收入
'total_cost' => '$' . number_format($item['total_cost'], 2), // 格式化总成本
'conversion_rate' => $conversion_rate,
'net_profit' => $net_profit,
'net_profit_margin' => $net_profit_margin,
'net_profit_on_ad_spend' => $net_profit_on_ad_spend,
];
}, $ads->toArray());
// Pagination 数据
$pagination = [
'startIndex' => ($page - 1) * $pageSize + 1, // 确保索引从 1 开始
'maxResults' => count($ads), // 当前页实际返回数据数量
'count' => count($allAds), // 符合条件的总记录数
'pageNo' => $page, // 当前页码
'pageSize' => $pageSize, // 每页条数
'pages' => (int)ceil(count($allAds) / $pageSize), // 总页数
];
return [
'pagination' => $pagination,
'statistics' => $statistics,
'data' => $result,
];
}
public static function getCreativeInsightData($platformType, $customerIds, $page, $pageSize, $keyword, $startDate = null, $endDate = null, $requireSpend = false, $countOnly = false)
{
// 1. 创建查询对象,初始化 BpsAdCreativeInsight 查询
$creativeDataQuery = BpsAdCreativeInsight::alias('i')
->join('bps.bps_ads_creative c', 'i.creative_id = c.creative_id', 'LEFT'); // 联接 bps_ads_creative 表
if ($platformType != 0) {
$creativeDataQuery->where('i.platform', $platformType); // 只有 platformType 不为 0 时才添加筛选
}
// 2. 日期范围筛选
if ($startDate && $endDate) {
$creativeDataQuery->whereBetween('i.date', [$startDate, $endDate]);
}
// 3. 客户 ID 过滤(如果提供了)
if (!empty($customerIds)) {
$creativeDataQuery->whereIn('i.account_id', $customerIds);
} else {
return $countOnly ? 0 : [
'data' => [],
'total' => 0,
'statistics' => [],
'pagination' => [
'startIndex' => 0,
'maxResults' => $pageSize,
'count' => 0,
'pageNo' => 1,
'pageSize' => $pageSize
]
];
}
// 仅返回记录数时的逻辑
if ($countOnly) {
return $creativeDataQuery->count('distinct(i.creative_id)') ?: 0;
}
// 4. 关键词过滤
if ($keyword) {
// $creativeDataQuery->where('c.name', 'like', '%' . $keyword . '%'); // 在 bps_ads_creative 表中查找关键词
$creativeDataQuery->whereRaw('LOWER(c.name) LIKE ?', ['%' . strtolower($keyword) . '%']); // 在 bps_ads_creative 表中查找关键词
}
// 5. 数据聚合(按 creative_id 和其他字段)
$creativeDataQuery->group('i.creative_id, i.platform, i.account_id, c.name, c.type,c.url, c.thumbnail_url') // 按需要的字段分组
->field([
'i.creative_id',
'i.platform',
'c.name AS creative_name', // 从 bps_ads_creative 表中选择 name
'c.url AS creative_url', // 从 bps_ads_creative 表中选择 url
'c.type AS creative_type', // 从 bps_ads_creative 表中选择 url
'c.thumbnail_url', // 从 bps_ads_creative 表中选择 thumbnail_url
ThinkDb::raw('COALESCE(SUM(i.spend) / 1000000, 0) AS total_spend'),
ThinkDb::raw('COALESCE(SUM(i.purchases_value) / 1000000, 0) AS total_purchases_value'),
ThinkDb::raw('COALESCE(SUM(i.purchases), 0) AS total_purchases'),
ThinkDb::raw('COALESCE(SUM(i.revenue) / 1000000, 0) AS total_revenue'),
ThinkDb::raw('COALESCE(SUM(i.impressions), 0) AS total_impressions'),
ThinkDb::raw('COALESCE(SUM(i.clicks), 0) AS total_clicks'),
ThinkDb::raw('COALESCE(SUM(i.link_clicks), 0) AS total_link_clicks'),
ThinkDb::raw('COALESCE(SUM(i.adds_to_cart), 0) AS total_adds_to_cart'),
ThinkDb::raw('COALESCE(SUM(i.outbound_clicks), 0) AS total_outbound_clicks'),
ThinkDb::raw('COALESCE(SUM(i.video_25),0) AS video_25'),
ThinkDb::raw('COALESCE(SUM(i.video_50),0) AS video_50'),
ThinkDb::raw('COALESCE(SUM(i.video_75),0) AS video_75'),
ThinkDb::raw('COALESCE(SUM(i.video_100),0) AS video_100'),
ThinkDb::raw('COALESCE(SUM(i.video_play),0) AS video_plays'),
ThinkDb::raw('-1 AS hold_rate')
]);
// 根据 $requireSpend 参数决定是否添加 SUM(i.spend) > 0 的条件
if ($requireSpend) {
$creativeDataQuery->having('SUM(i.spend) > 0');
}
// 6. 执行查询并获取聚合结果
// $aggregatedData = $creativeDataQuery->select();
// 6. 执行查询并获取聚合结果
$aggregatedData = $creativeDataQuery->select()->toArray(); // 转换为数组处理
$totalRecords = count($aggregatedData); // 总记录数
// 计算分页起始索引
$startIndex = ($page - 1) * $pageSize;
// 打印调试 SQL 查询
//$sql = $creativeDataQuery->getLastSql();
// dump($sql);
// 7. 初始化广告创意的汇总数据和统计数据
$creativeSummaryData = [];
// $statisticsData = $this->initializeStatistics();
$statisticsData = [];
// 8. 遍历查询结果并计算每个 creative 的相关统计数据
foreach ($aggregatedData as $creativeData) {
// 初始化该 creative_id 的数据(如果不存在)
$creativeId = $creativeData['creative_id'];
if (!isset($creativeSummaryData[$creativeId])) {
$creativeSummaryData[$creativeId] = [
'creative_id' => $creativeData['creative_id'],
'platform' => $creativeData['platform'],
'creative' => $creativeData['creative_name'],
'creative_type' => $creativeData['creative_type'],
'creative_url' => $creativeData['creative_url'] ?? '',
'thumbnail_url' => $creativeData['thumbnail_url'] ?? '',
'title' => $creativeData['title'] ?? '',
'spend' => 0,
'purchases_value' => 0,
'roas' => 0,
'cpa' => '-',
'cpc_link_click' => '-',
'cpm' => '-',
'cpc_all' => '-',
'aov' => '-',
'click_to_atc_ratio' => '-',
'atc_to_purchase_ratio' => '-',
'purchases' => 0,
'first_frame_retention' => '-',
'thumbstop' => '-',
'ctr_outbound' => '-',
'click_to_purchase' => '-',
'ctr_all' => '-',
'video_25' => 0,
'video_50' => 0,
'video_75' => 0,
'video_100' => 0,
'video_plays' => 0,
'video_plays_25_rate' => '-',
'video_plays_50_rate' => '-',
'video_plays_75_rate' => '-',
'video_plays_100_rate' => '-',
'hold_rate' => '-',
'total_conversions_value' => 0,
'total_conversions' => 0,
'impressions' => 0,
'clicks' => 0,
'link_clicks' => 0,
'outbound_clicks' => 0,
'adds_to_cart' => 0,
'revenue' => 0,
'ad_count' => 0
];
}
// 更新该 creative_id 的统计数据
$creativeSummaryData[$creativeId]['spend'] += $creativeData['total_spend'];
$creativeSummaryData[$creativeId]['purchases_value'] += $creativeData['total_purchases_value'];
$creativeSummaryData[$creativeId]['purchases'] += $creativeData['total_purchases'];
$creativeSummaryData[$creativeId]['impressions'] += $creativeData['total_impressions'];
$creativeSummaryData[$creativeId]['clicks'] += $creativeData['total_clicks'];
$creativeSummaryData[$creativeId]['link_clicks'] += $creativeData['total_link_clicks'];
$creativeSummaryData[$creativeId]['outbound_clicks'] += $creativeData['total_outbound_clicks'];
$creativeSummaryData[$creativeId]['adds_to_cart'] += $creativeData['total_adds_to_cart'];
$creativeSummaryData[$creativeId]['revenue'] += $creativeData['total_revenue'];
$creativeSummaryData[$creativeId]['video_25'] += $creativeData['video_25'];
$creativeSummaryData[$creativeId]['video_50'] += $creativeData['video_50'];
$creativeSummaryData[$creativeId]['video_75'] += $creativeData['video_75'];
$creativeSummaryData[$creativeId]['video_100'] += $creativeData['video_100'];
$creativeSummaryData[$creativeId]['video_plays'] += $creativeData['video_plays'];
// 填充广告计数
// $creativeSummaryData[$creativeData->creative_id]['ad_count'] = rand(10, 200); // 每个 creative_id 对应一个广告
}
// dump($creativeSummaryData);
// 汇总总体统计数据
$statisticsData['spend'] = array_sum(array_column($creativeSummaryData, 'spend'));
$statisticsData['purchases_value'] = array_sum(array_column($creativeSummaryData, 'purchases_value'));
$statisticsData['purchases'] = array_sum(array_column($creativeSummaryData, 'purchases'));
$statisticsData['impressions'] = array_sum(array_column($creativeSummaryData, 'impressions'));
$statisticsData['clicks'] = array_sum(array_column($creativeSummaryData, 'clicks'));
$statisticsData['link_clicks'] = array_sum(array_column($creativeSummaryData, 'link_clicks'));
$statisticsData['outbound_clicks'] = array_sum(array_column($creativeSummaryData, 'outbound_clicks'));
$statisticsData['adds_to_cart'] = array_sum(array_column($creativeSummaryData, 'adds_to_cart'));
$statisticsData['revenue'] = array_sum(array_column($creativeSummaryData, 'revenue'));
$statisticsData['video_25'] = array_sum(array_column($creativeSummaryData, 'video_25'));
$statisticsData['video_50'] = array_sum(array_column($creativeSummaryData, 'video_50'));
$statisticsData['video_75'] = array_sum(array_column($creativeSummaryData, 'video_75'));
$statisticsData['video_100'] = array_sum(array_column($creativeSummaryData, 'video_100'));
$statisticsData['video_plays'] = array_sum(array_column($creativeSummaryData, 'video_plays'));
// 汇总统计数据
$statistics = [
'spend' => '$' . number_format($statisticsData['spend'], 2), // 格式化金额
'purchases_value' => '$' . number_format($statisticsData['purchases_value'], 2), // 格式化金额
'roas' => $statisticsData['spend'] == 0 ? '-' : round($statisticsData['purchases_value'] / $statisticsData['spend'], 2) . 'x',
'cpa' => $statisticsData['purchases'] == 0 ? '-' : '$' . number_format($statisticsData['spend'] / $statisticsData['purchases'], 2),
'cpc_link_click' => $statisticsData['link_clicks'] == 0 ? '-' : '$' . number_format($statisticsData['spend'] / $statisticsData['link_clicks'], 2),
'cpm' => $statisticsData['impressions'] == 0 ? '-' : '$' . number_format($statisticsData['spend'] * 1000 / $statisticsData['impressions'], 2), // sum(spend) * 1000 / sum(Impression)
'cpc_all' => $statisticsData['clicks'] == 0 ? '-' : '$' . number_format($statisticsData['spend'] / $statisticsData['clicks'], 2),
'aov' => $statisticsData['purchases'] == 0 ? '-' : '$' . number_format($statisticsData['purchases_value'] / $statisticsData['purchases'], 2), // 格式化金额
'click_to_atc_ratio' => ($statisticsData['clicks'] > 0) ? number_format(($statisticsData['adds_to_cart'] / $statisticsData['clicks']) * 100, 2) . '%' : '-',
'atc_to_purchase_ratio' => ($statisticsData['adds_to_cart'] > 0) ? number_format(($statisticsData['purchases'] / $statisticsData['adds_to_cart']) * 100, 2) . '%' : '-',
'purchases' => $statisticsData['purchases'],
'first_frame_retention' => self::calculateFirstFrameRetention($statisticsData['impressions'], $statisticsData['video_plays'], $statisticsData['video_25']),
'thumbstop' => self::calculateThumbstop($statisticsData['impressions'], $statisticsData['video_plays'], $statisticsData['video_25']),
'ctr_outbound' => ($statisticsData['impressions'] > 0) ? number_format(($statisticsData['outbound_clicks'] / $statisticsData['impressions']) * 100, 2) . '%' : '-', // 格式化为百分比
'click_to_purchase' => ($statisticsData['purchases'] > 0) ? number_format(($statisticsData['outbound_clicks'] / $statisticsData['purchases']) * 100, 2) . '%' : '-',
'ctr_all' => ($statisticsData['impressions'] > 0) ? number_format(($statisticsData['clicks'] / $statisticsData['impressions']) * 100, 2) . '%' : '-', // 格式化为百分比
'video_plays_25_rate' => ($statisticsData['video_plays'] > 0) ? number_format(($statisticsData['video_25'] / $statisticsData['video_plays']) * 100, 2) . '%' : '-',
'video_plays_50_rate' => ($statisticsData['video_plays'] > 0) ? number_format(($statisticsData['video_50'] / $statisticsData['video_plays']) * 100, 2) . '%' : '-',
'video_plays_75_rate' => ($statisticsData['video_plays'] > 0) ? number_format(($statisticsData['video_75'] / $statisticsData['video_plays']) * 100, 2) . '%' : '-',
'video_plays_100_rate' => ($statisticsData['video_plays'] > 0) ? number_format(($statisticsData['video_100'] / $statisticsData['video_plays']) * 100, 2) . '%' : '-',
'hold_rate' => self::calculateHoldRate($statisticsData['video_25'], $statisticsData['video_50'], $statisticsData['video_75'], $statisticsData['video_100']),// 格式化百分比
];
// 获取每个 creative_id 对应的广告数量
$creativeIds_from_google = array_keys(array_filter($creativeSummaryData, function ($item) {
return $item['platform'] == 2;
}));
if (!empty($creativeIds_from_google)) {
// 将整数日期转为 Y-m-d 格式
$formattedStartDate = DateTime::createFromFormat('Ymd', $startDate)->format('Y-m-d');
$formattedEndDate = DateTime::createFromFormat('Ymd', $endDate)->format('Y-m-d');
// 查询满足条件的 asset_id 和唯一 ad_id 计数
$ad_count_google = AssetRelation::whereIn('asset_id', $creativeIds_from_google)
->whereBetween('date', [$formattedStartDate, $formattedEndDate])
->group('asset_id')
->column('COUNT(DISTINCT ad_id) AS ad_count', 'asset_id');
}
$creativeIds_from_fb_tk = array_keys(array_filter($creativeSummaryData, function ($item) {
return ($item['platform'] == 1 || $item['platform'] == 3);
}));
if (!empty($creativeIds_from_fb_tk)) {
$ad_count_fb_tk = BpsAdAd::whereIn('creative_id', $creativeIds_from_fb_tk)
->group('creative_id')
->column('COUNT(DISTINCT ad_id) as ad_count', 'creative_id');
}
// 确保变量存在,避免未定义错误
$ad_count_fb_tk = $ad_count_fb_tk ?? [];
$ad_count_google = $ad_count_google ?? [];
// 合并数组
$ad_count_combined = $ad_count_fb_tk + $ad_count_google;
//dump($ad_count_combined);
// dump($creativeIds_from_fb_tk);
// 将广告数量填充到对应的 creativeSummaryData 中
foreach ($creativeSummaryData as $creativeId => &$creativeData) {
$creativeData['ad_count'] = $ad_count_combined[$creativeId] ?? 0; // 如果没有匹配记录,默认填充 0
// dump($creativeData);
}
unset($creativeData); // 解除引用
// dump($creativeSummaryData);
// 格式化返回的创意数据
$formattedData = array_map(function ($item) {
return [
'creative_id' => $item['creative_id'],
'creative' => $item['creative'] ?: '-',
'platform' => $item['platform'] ?: '-',
'creative_type' => $item['creative_type'],
'creative_url' => $item['creative_url'],
'thumbnail_url' => $item['thumbnail_url'],
'spend' => '$' . number_format($item['spend'], 2),
'purchases_value' => '$' . number_format($item['purchases_value'], 2),
'roas' => $item['spend'] == 0 ? '-' : round($item['purchases_value'] / $item['spend'], 2) . 'x',
'cpa' => $item['purchases'] == 0 ? '-' : '$' . number_format($item['spend'] / $item['purchases'], 2),
'cpc_link_click' => $item['link_clicks'] == 0 ? '-' : '$' . number_format($item['spend'] / $item['link_clicks'], 2),
'cpm' => $item['impressions'] == 0 ? '-' : '$' . number_format($item['spend'] * 1000 / $item['impressions'], 2),
'cpc_all' => $item['clicks'] == 0 ? '-' : '$' . number_format($item['spend'] / $item['clicks'], 2),
'aov' => $item['purchases'] == 0 ? '-' : '$' . number_format($item['purchases_value'] / $item['purchases'], 2),
'click_to_atc_ratio' => ($item['clicks'] > 0) ? number_format(($item['adds_to_cart'] / $item['clicks']) * 100, 2) . '%' : '-',
'atc_to_purchase_ratio' => ($item['adds_to_cart'] > 0) ? number_format(($item['purchases'] / $item['adds_to_cart']) * 100, 2) . '%' : '-',
'purchases' => $item['purchases'],
'first_frame_retention' => self::calculateFirstFrameRetention($item['impressions'], $item['video_plays'], $item['video_25']),
'thumbstop' => self::calculateThumbstop($item['impressions'], $item['video_plays'], $item['video_25']),
'ctr_outbound' => ($item['impressions'] > 0) ? number_format(($item['outbound_clicks'] / $item['impressions']) * 100, 2) . '%' : '-', // 格式化为百分比
'click_to_purchase' => ($item['purchases'] > 0) ? number_format(($item['outbound_clicks'] / $item['purchases']) * 100, 2) . '%' : '-',
'ctr_all' => ($item['impressions'] > 0) ? number_format(($item['clicks'] / $item['impressions']) * 100, 2) . '%' : '-',
'total_conversions_value' => '$' . number_format($item['purchases_value'], 2), //准备删除
'impressions' => $item['impressions'],
'video_plays_25_rate' => ($item['video_plays'] > 0) ? number_format($item['video_25'] / $item['video_plays'], 2) . '%' : '-',
'video_plays_50_rate' => ($item['video_plays'] > 0) ? number_format($item['video_50'] / $item['video_plays'], 2) . '%' : '-',
'video_plays_75_rate' => ($item['video_plays'] > 0) ? number_format($item['video_75'] / $item['video_plays'], 2) . '%' : '-',
'video_plays_100_rate' => ($item['video_plays'] > 0) ? number_format($item['video_100'] / $item['video_plays'], 2) . '%' : '-',
'hold_rate' => self::calculateHoldRate($item['video_25'], $item['video_50'], $item['video_75'], $item['video_100']),
'ad_count' => $item['ad_count'],
// 添加更多的格式化字段
];
}, $creativeSummaryData);
// $formattedData = array_values($creativeSummaryData); // 未分页处理
$pagedData = array_slice(array_values($formattedData), $startIndex, $pageSize); // 分页处理
// 9. 返回分页数据
return [
'data' => $pagedData,
'total' => count($creativeSummaryData),
'statistics' => $statistics, // 汇总的统计数据
'pagination' => [
'startIndex' => ($page - 1) * $pageSize,
'maxResults' => $pageSize,
'count' => count($creativeSummaryData),
'pageNo' => $page,
'pageSize' => $pageSize,
'pages' => ceil(count($creativeSummaryData) / $pageSize)
]
];
}
public static function calculateFirstFrameRetention($impressions, $video_play, $video_25)
{
// 如果 impressions 为 0 或 NULL直接返回 0
if ($impressions == 0 || $video_play == 0) {
return '-';
}
// 处理 NULL 值,用 0 替换
$video_25 = $video_25 ?? 0;
// 计算公式
return number_format(($video_play / $impressions) * ($video_25 / $video_play) * 100, 2) . '%';
}
public static function calculateThumbstop($impressions, $video_play, $video_25)
{
// 如果 impressions 为 0 或 NULL直接返回 0
if ($impressions == 0) {
return '-';
}
// 处理 NULL 值,用 0 替换
$video_play = $video_play ?? 0;
$video_25 = $video_25 ?? 0;
// 计算公式
return number_format((($video_play * 0.7) + ($video_25 * 0.3)) / $impressions * 100, 2) . '%';
}
public static function calculateHoldRate($video_25, $video_50, $video_75, $video_100)
{
// 如果 video_25 为 0 或 NULL直接返回 0
if ($video_25 == 0) {
return '-';
}
// 处理 NULL 值,用 0 替换
$video_25 = $video_25 ?? 0;
$video_50 = $video_50 ?? 0;
$video_75 = $video_75 ?? 0;
$video_100 = $video_100 ?? 0;
// 计算公式
$numerator = $video_25 + $video_50 + $video_75 + $video_100;
$denominator = $video_25 * 4;
return number_format(($numerator / $denominator) * 100, 2) . '%';
}
/**
* 初始化统计数据
*/
private function initializeStatistics()
{
return [
'impressions' => 0,
'clicks' => 0,
'revenue' => 0,
'spend' => 0,
'purchases_value' => 0,
'roas' => 0,
'cpa' => '-',
'cpc_link_click' => '-',
'cpm' => '-',
'cpc_all' => '-',
'aov' => '-',
'click_to_atc_ratio' => '-',
'atc_to_purchase_ratio' => '-',
'purchases' => 0,
'first_frame_retention' => '-',
'thumbstop' => '-',
'ctr_outbound' => '-',
'click_to_purchase' => '-',
'ctr_all' => '-',
'video_plays_25_rate' => 0,
'video_plays_50_rate' => 0,
'video_plays_75_rate' => 0,
'video_plays_100_rate' => 0,
'hold_rate' => '-',
// Add other stats as necessary
];
}
public function getAdcycleInsight($platformType, $customerIds, $cycle, $startDate = null, $endDate = null)
{
// 1. 查询全部数据集
$adcycleDataQuery = BpsAdInsight::alias('i');
if ($platformType != 0) {
$adcycleDataQuery->where('i.platform', $platformType); // 只有 platformType 不为 0 时才添加筛选
}
//dump($customerIds);
// 2. 客户 ID 过滤(如果提供了)
if (!empty($customerIds)) {
$adcycleDataQuery->whereIn('i.account_id', $customerIds);
} else {
return [
'data' => [],
'total' => 0
];
}
// 3. 时间范围筛选
if ($startDate && $endDate) {
$adcycleDataQuery->whereBetween('i.date', [$startDate, $endDate]);
}
// 4. 获取数据并按日期聚合
$adcycleData = $adcycleDataQuery->field([
'i.date',
ThinkDb::raw('COALESCE(SUM(i.spend) / 1000000, 0) as spend'),
ThinkDb::raw('COALESCE(SUM(i.revenue), 0) as revenue')
])
->group('i.date') // 按日期进行分组
->select();
// dump(ThinkDb::getLastSql());
// 5. 处理数据,按照周期进行分组
$processedData = $this->processDataByCycle($adcycleData->toArray(), $cycle);
// 6. 返回处理后的数据
return $processedData;
}
/**
* 按周期处理数据
*
* @param array $adcycleData 原始数据
* @param string $cycle 周期类型daily, weekly, monthly
* @return array 按照周期分组后的数据
*/
private function processDataByCycle($adcycleData, $cycle)
{
// 排序 adcycleData 数组,确保按照日期升序排列
usort($adcycleData, function ($a, $b) {
return $a['date'] <=> $b['date']; // 按照 'YYYYMMDD' 字符顺序排序
});
$groupedData = [];
foreach ($adcycleData as $data) {
// 根据周期类型,调整日期分组
switch ($cycle) {
case 1:
// 将 'YYYYMMDD' 格式的日期转换为 'YYYY-MM-DD'
$date = DateTime::createFromFormat('Ymd', $data['date']); // 转换为 DateTime 对象
$key = $date ? $date->format('Y-m-d') : ''; // 格式化为 'YYYY-MM-DD'
break;
case 2:
// 使用 ISO 周格式来分组
$key = $this->getWeekFromDate($data['date']);
break;
case 3:
// 按年-月格式分组
$key = $this->getMonthFromDate($data['date']);
break;
default:
throw new \InvalidArgumentException("Invalid cycle value. Use 'daily', 'weekly' or 'monthly'.");
}
// 汇总数据
if (!isset($groupedData[$key])) {
$groupedData[$key] = [
'spend' => 0,
'revenue' => 0,
// 'conversions_value' => 0,
// 'conversions' => 0,
// 'impressions' => 0,
// 'clicks' => 0
];
}
// 累加数据
$groupedData[$key]['spend'] += $data['spend'];
$groupedData[$key]['revenue'] += $data['revenue'];
// $groupedData[$key]['conversions_value'] += $data->purchases_value;
// $groupedData[$key]['conversions'] += $data->purchases;
// $groupedData[$key]['impressions'] += $data->impressions;
// $groupedData[$key]['clicks'] += $data->clicks;
}
// 格式化返回数据
$formattedData = [];
foreach ($groupedData as $key => $values) {
$roas = $values['spend'] > 0 ? $values['revenue'] / $values['spend'] : 0;
$formattedData[] = [
'date' => $key,
'spend' => round($values['spend'], 2),
'roas' => $roas > 0 ? round($roas, 2) : 0
];
}
// 如果没有数据,返回空数组并提供默认的分页信息
if (empty($formattedData)) {
return [
'data' => [],
'total' => 0,
];
}
// 返回格式化后的数据
return [
'data' => $formattedData,
'total' => count($formattedData),
];
}
/**
* 获取周数(基于 ISO 周格式)
*
* @param int $date 日期格式YYYYMMDD
* @return string ISO 周格式YYYY-Www
*/
private function getWeekFromDate($date)
{
// 将日期转换为 PHP DateTime 对象
$dateStr = (string)$date;
$dateObj = \DateTime::createFromFormat('Ymd', $dateStr);
// 获取 ISO 周格式
return $dateObj->format('o-W') . 'W';
}
/**
* 获取月分格式YYYY-MM
*
* @param int $date 日期格式YYYYMMDD
* @return string 月份格式YYYY-MM
*/
private function getMonthFromDate($date)
{
// 将日期转换为 PHP DateTime 对象
$dateStr = (string)$date;
$dateObj = \DateTime::createFromFormat('Ymd', $dateStr);
// 获取年-月格式
return $dateObj->format('Y-m');
}
public static function getPlatformType($thirdType)
{
$platformMapping = [
'facebook' => 1,
'google' => 2,
'tiktok' => 3,
];
return $platformMapping[$thirdType] ?? null; // 如果不存在的第三方类型,返回 null
}
private static function getLastQueryParams()
{
$query = ThinkDb::getLastQuery();
return $query ? $query->getOptions()['where'] : [];
}
// 修改enableSqlLog方法
public static function enableSqlLog()
{
if (!self::$sqlLogEnabled) {
ThinkDb::listen(function($sql, $time, $explain) {
$logData = [
'sql' => $sql,
'time' => $time . 'ms',
'params' => self::getLastQueryParams(),
'trace' => debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 5)
];
// 仅在非空时记录 EXPLAIN
if (!empty($explain)) {
$logData['explain'] = $explain;
}
Log::channel('sql')->debug('SQL', $logData);
});
self::$sqlLogEnabled = true;
}
}
}