From a3768b694ed73d39c3eb8746c8b4d14b63c06a32 Mon Sep 17 00:00:00 2001 From: huangguancheng Date: Thu, 9 Jan 2025 18:10:10 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=8E=9F=E5=9E=8B=E7=9A=84accounts?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3=E3=80=81campaign=E6=8E=A5=E5=8F=A3=E3=80=81a?= =?UTF-8?q?dset=E6=8E=A5=E5=8F=A3=E3=80=81ad=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/controller/BpsAdController.php | 553 ++++++++++++++++ app/model/BpsAdAd.php | 195 ++++++ app/model/BpsAdCampaign.php | 118 ++++ app/model/BpsAdInsight.php | 93 +++ app/model/BpsAdSet.php | 106 +++ app/service/AdsInsightService.php | 995 ++++++++++++++++++++++++++++ app/service/BpsAdAccountService.php | 271 ++++++++ app/service/GoogleOAuthService.php | 2 +- config/route.php | 68 +- 9 files changed, 2394 insertions(+), 7 deletions(-) create mode 100644 app/controller/BpsAdController.php create mode 100644 app/model/BpsAdAd.php create mode 100644 app/model/BpsAdCampaign.php create mode 100644 app/model/BpsAdInsight.php create mode 100644 app/model/BpsAdSet.php create mode 100644 app/service/AdsInsightService.php create mode 100644 app/service/BpsAdAccountService.php diff --git a/app/controller/BpsAdController.php b/app/controller/BpsAdController.php new file mode 100644 index 0000000..c7d31a3 --- /dev/null +++ b/app/controller/BpsAdController.php @@ -0,0 +1,553 @@ +all(); + $options['jwtClaims'] = $request->jwtClaims; + + // 获取请求参数 + $page = $options['pageNo'] ?? 1; // 页码 + $pageSize = $options['pageSize'] ?? 1000; // 每页数量 + $keyword = $options['conditions']['keyword'] ?? ''; // 关键字搜索 + $platformType = $options['conditions']['platformType'] ?? 0; // 平台类型 + $startDate = $options['conditions']['startDate'] ?? null; // 开始日期 + $endDate = $options['conditions']['endDate'] ?? null; // 结束日期 +// dump($options); + // 根据 platformType 获取第三方用户数据 + if ($platformType === 1) { + // 获取 Facebook 第三方用户数据 + $users = $this->bpsAdAccountService->getMetaThirdUsers(['uid' => $options['jwtClaims']['uid']]); + } elseif ($platformType === 2) { + // 获取 Google 第三方用户数据 + $users = $this->bpsAdAccountService->getGoogleThirdUsers(['uid' => $options['jwtClaims']['uid']]); + } elseif ($platformType === 3) { + // 获取 TikTok 第三方用户数据 + $users = $this->bpsAdAccountService->getTiktokThirdUsers(['uid' => $options['jwtClaims']['uid']]); + } else { + // 根据 JWT claims 获取所有平台的第三方用户 + $users = $this->bpsAdAccountService->getAllThirdUsers(['uid' => $options['jwtClaims']['uid']]); + } + + if (empty($users)) { + return $this->errorResponse(1, 'no data'); + } + +// 获取客户 ID 数组,并去重 + $userIds = array_unique(array_column($users, 'id')); + + + // 调用 Service 层查询第三方用户数据列表 + $result = $this->adsInsightService::getThirdUserList( + $platformType, // 平台类型 + $userIds, // 第三方用户 ID 数组 + $page, // 页码 + $pageSize, // 每页数量 + $keyword, // 关键字 + $startDate, // 开始日期 + $endDate // 结束日期 + ); + + // 返回结果 + return $this->successResponse($result, $request); + } + + + public function listAds(Request $request) + { + $options = $request->all(); + $options['jwtClaims'] = $request->jwtClaims; + + // 获取请求参数 + $page = $options['pageNo'] ?? 1; // 页码 + $pageSize = $options['pageSize'] ?? 1000; // 每页数量 + $keyword = $options['conditions']['keyword'] ?? ''; // 关键字搜索 + $platformType = $options['conditions']['platformType'] ?? 0; // 平台类型 + $startDate = $options['conditions']['startDate'] ?? null; // 开始日期 + $endDate = $options['conditions']['endDate'] ?? null; // 结束日期 +// $dateRange = 'Last Week'; // 默认日期范围 + + // 根据 platformType 获取广告账户 + if ($platformType === 1) { + $accounts = $this->bpsAdAccountService->getMetaAdAccounts(['refresh_token' => $request->refresh_token]); + } elseif ($platformType === 2) { + $accounts = $this->bpsAdAccountService->getGoogleAdAccounts(['refresh_token' => $request->refresh_token]); + } elseif ($platformType === 3) { + $accounts = $this->bpsAdAccountService->getTiktokAdAccounts(['refresh_token' => $request->refresh_token]); + } else { + // TODO: 匹配jwt的商户id还是登录用户id + $accounts = $this->bpsAdAccountService->getAllAdAccounts(['uid' => $options['jwtClaims']['uid']]); + } + + if (empty($accounts)) { + return $this->errorResponse(1, 'no data'); + } + + // 获取客户ID数组 + $accountIds = array_column($accounts, 'account_id'); + + // 调用 Service 层查询广告列表 + $result = $this->adsInsightService::getAdList( + $platformType, // 平台类型 + $accountIds, // 客户 ID 数组 + $page, // 页码 + $pageSize, // 每页数量 + $keyword, // 关键字 + $startDate, // 开始日期 + $endDate // 结束日期 + ); + + // 返回结果 + return $this->successResponse($result, $request); + } + + + public function listAssets(Request $request) + { + $options = $request->all(); + + // 获取请求参数 + $page = $options['pageNo'] ?? 1; // 页码 + $pageSize = $options['pageSize'] ?? 10; // 每页数量 + $keyword = $options['conditions']['keyword'] ?? ''; // 关键字搜索 + $startDate = $options['conditions']['startDate'] ?? null; // 开始日期 + $endDate = $options['conditions']['endDate'] ?? null; // 结束日期 + $dateRange = 'Last Week'; // 默认日期范围 + +// $customerId = 4060397299; + $customers = $this->googleOAuthService->getGoogleAdCustomers(['refresh_token' => $request->refresh_token]); + $customerIds = array_column($customers, 'customer_id'); +//dump( $customerIds, // 客户 ID 数组 +// $page, // 页码 +// $pageSize, // 每页数量 +// $keyword, // 关键字 +// $dateRange, +// $startDate, // 开始日期 +// $endDate); + // 调用 Service 层查询 + $result = $this->googleAdsReportService->getAssetConversionData( + $customerIds, // 客户 ID 数组 + $page, // 页码 + $pageSize, // 每页数量 + $keyword, // 关键字 + $dateRange, + $startDate, // 开始日期 + $endDate); + return $this->successResponse($result, $request); + } + + public function listCampaigns(Request $request) + { + $options = $request->all(); + $options['jwtClaims'] = $request->jwtClaims; + + // 获取请求参数 + $page = $options['pageNo'] ?? 1; // 页码 + $pageSize = $options['pageSize'] ?? 1000; // 每页数量 + $keyword = $options['conditions']['keyword'] ?? ''; // 关键字搜索 + $platformType = $options['conditions']['platformType'] ?? 0; // 关键字搜索 + $startDate = $options['conditions']['startDate'] ?? null; // 开始日期 + $endDate = $options['conditions']['endDate'] ?? null; // 结束日期 +// $dateRange = 'Last Week'; // 默认日期范围 + // 根据 platformType 获取广告账户 + if ($platformType === 1) { + $accounts = $this->bpsAdAccountService->getMetaAdAccounts(['refresh_token' => $request->refresh_token]); + } elseif ($platformType === 2) { + $accounts = $this->bpsAdAccountService->getGoogleAdAccounts(['refresh_token' => $request->refresh_token]); + } elseif ($platformType === 3) { + $accounts = $this->bpsAdAccountService->getTiktokAdAccounts(['refresh_token' => $request->refresh_token]); + } else { + // TODO: 匹配jwt的商户id还是登录用户id + $accounts = $this->bpsAdAccountService->getAllAdAccounts(['uid' => $options['jwtClaims']['uid']]); + } + + if (empty($accounts)) { + return $this->errorResponse(1, 'no data'); + } + + // 获取客户ID数组 + $accountIds = array_column($accounts, 'account_id'); + // 调用 Service 层查询 + $result = $this->adsInsightService::getCampaignList( + $platformType, + $accountIds, // 客户 ID 数组 + $page, // 页码 + $pageSize, // 每页数量 + $keyword, // 关键字 + $startDate, // 开始日期 + $endDate // 结束日期 + ); + return $this->successResponse($result, $request); +// return $this->errorResponse(300,'授权失败'); + } + + public function listAdsets(Request $request) + { + $options = $request->all(); + $options['jwtClaims'] = $request->jwtClaims; + + // 获取请求参数 + $page = $options['pageNo'] ?? 1; // 页码 + $pageSize = $options['pageSize'] ?? 1000; // 每页数量 + $keyword = $options['conditions']['keyword'] ?? ''; // 关键字搜索 + $platformType = $options['conditions']['platformType'] ?? 0; // 平台类型 + $startDate = $options['conditions']['startDate'] ?? null; // 开始日期 + $endDate = $options['conditions']['endDate'] ?? null; // 结束日期 +// $dateRange = 'Last Week'; // 默认日期范围 + + // 根据 platformType 获取广告账户 + if ($platformType === 1) { + $accounts = $this->bpsAdAccountService->getMetaAdAccounts(['refresh_token' => $request->refresh_token]); + } elseif ($platformType === 2) { + $accounts = $this->bpsAdAccountService->getGoogleAdAccounts(['refresh_token' => $request->refresh_token]); + } elseif ($platformType === 3) { + $accounts = $this->bpsAdAccountService->getTiktokAdAccounts(['refresh_token' => $request->refresh_token]); + } else { + // TODO: 匹配jwt的商户id还是登录用户id + $accounts = $this->bpsAdAccountService->getAllAdAccounts(['uid' => $options['jwtClaims']['uid']]); + } + + if (empty($accounts)) { + return $this->errorResponse(1, 'no data'); + } + + // 获取客户ID数组 + $accountIds = array_column($accounts, 'account_id'); + + // 调用 Service 层查询广告组列表 + $result = $this->adsInsightService::getAdsetList( + $platformType, + $accountIds, // 客户 ID 数组 + $page, // 页码 + $pageSize, // 每页数量 + $keyword, // 关键字 + $startDate, // 开始日期 + $endDate // 结束日期 + ); + + // 返回结果 + return $this->successResponse($result, $request); + } + + + public function exportAdsToExcel(Request $request) + { + $options = $request->all(); + + // 获取请求参数 + $keyword = $options['conditions']['keyword'] ?? ''; // 关键字搜索 + $startDate = $options['conditions']['startDate'] ?? null; // 开始日期 + $endDate = $options['conditions']['endDate'] ?? null; // 结束日期 + $dateRange = 'Last Week'; // 默认日期范围 + + $customers = $this->googleOAuthService->getGoogleAdCustomers(['refresh_token' => $request->refresh_token]); + $customerIds = array_column($customers, 'customer_id'); + // 调用 service 层导出数据 + return $this->googleAdsReportService::exportAdListToExcel($customerIds, $keyword, $dateRange, $startDate, $endDate); + } + + public function exportCampaignsToExcel(Request $request) + { + $options = $request->all(); + // 获取请求参数 + $keyword = $options['conditions']['keyword'] ?? ''; // 关键字搜索 + $startDate = $options['conditions']['startDate'] ?? null; // 开始日期 + $endDate = $options['conditions']['endDate'] ?? null; // 结束日期 + $dateRange = 'Last Week'; // 默认日期范围 + + $customers = $this->googleOAuthService->getGoogleAdCustomers(['refresh_token' => $request->refresh_token]); + $customerIds = array_column($customers, 'customer_id'); +// dump($customerIds); + // 调用 service 层导出数据 + return $this->googleAdsReportService->exportCampaignsToExcel($customerIds, $keyword, $dateRange, $startDate, $endDate); + } + + public function exportGroupsToExcel(Request $request) + { + $options = $request->all(); + // 获取请求参数 + $keyword = $options['conditions']['keyword'] ?? ''; // 关键字搜索 + $startDate = $options['conditions']['startDate'] ?? null; // 开始日期 + $endDate = $options['conditions']['endDate'] ?? null; // 结束日期 + $dateRange = 'Last Week'; // 默认日期范围 + + $customers = $this->googleOAuthService->getGoogleAdCustomers(['refresh_token' => $request->refresh_token]); + $customerIds = array_column($customers, 'customer_id'); + + // 你可以进一步验证日期格式(可选) +// if ($startDate && !strtotime($startDate)) { +// return response()->json(['error' => 'Invalid start date format'], 400); +// } +// if ($endDate && !strtotime($endDate)) { +// return response()->json(['error' => 'Invalid end date format'], 400); +// } + + // 调用 service 层导出数据 + return $this->googleAdsReportService->exportAdGroupsToExcel($customerIds, $keyword, $dateRange, $startDate, $endDate); + } + + public function listGroups(Request $request) + { + $options = $request->all(); + + // 获取请求参数 + $page = $options['pageNo'] ?? 1; // 页码 + $pageSize = $options['pageSize'] ?? 10; // 每页数量 + $keyword = $options['conditions']['keyword'] ?? ''; // 关键字搜索 + $startDate = $options['conditions']['startDate'] ?? null; // 开始日期 + $endDate = $options['conditions']['endDate'] ?? null; // 结束日期 + $dateRange = 'Last Week'; // 默认日期范围 + + $customers = $this->googleOAuthService->getGoogleAdCustomers(['refresh_token' => $request->refresh_token]); + $customerIds = array_column($customers, 'customer_id'); + // 调用 Service 层查询 + $result = $this->googleAdsReportService::getAdGroupList( + $customerIds, // 客户 ID 数组 + $page, // 页码 + $pageSize, // 每页数量 + $keyword, // 关键字 + $dateRange, + $startDate, // 开始日期 + $endDate + ); + return $this->successResponse($result, $request); + } + + + /** + * 获取广告系列的状态 备用 + */ +// public function getCampaignStatus(int $campaignId): Response +// { +// try { +// $status = $this->googleAdsCampaignService->getCampaignStatus($campaignId); +// return $this->successResponse(['status' => $status]); +// } catch (ValidateException $e) { +// return $this->errorResponse(400, $e->getMessage()); +// } +// } + + /** + * 更新广告系列的状态 + */ + public function updateCampaignStatus(Request $request): Response + { + $campaignStatus = [ + 0, // UNSPECIFIED + 1, // UNKNOWN + 2, // ENABLED + 3, // PAUSED + 4, // REMOVED + ]; + $requestData = $request->all(); // 获取请求数据 + $requestData['refresh_token'] = $request->refresh_token; + $requestData['login_customer_id'] = $request->login_customer_id; +// dump($requestData); + + $status = $requestData['status']; + if (!in_array($status, $campaignStatus)) { + return $this->errorResponse(101, 'status参数错误'); + } +// try { + $reslut = $this->googleAdsCampaignService->updateCampaignStatus($requestData); + if (!$reslut) { + return $this->errorResponse(101, 'Status update failed'); + } + return $this->successResponse(['message' => 'Status updated successfully'], $request); +// } catch (ValidateException $e) { +// return $this->errorResponse(400, $e->getMessage()); +// } + } + + /** + * 判断广告系列是否启用 + */ +// public function isEnabled(int $campaignId): Json +// { +// try { +// $isEnabled = $this->campaignService->isCampaignEnabled($campaignId); +// return json(['enabled' => $isEnabled], 200); +// } catch (ValidateException $e) { +// return json(['error' => $e->getMessage()], 400); +// } +// } + + /** + * 判断广告系列是否暂停 + */ +// public function isPaused(int $campaignId): Json +// { +// try { +// $isPaused = $this->campaignService->isCampaignPaused($campaignId); +// return json(['paused' => $isPaused], 200); +// } catch (ValidateException $e) { +// return json(['error' => $e->getMessage()], 400); +// } +// } + + /** + * 判断广告系列是否停止 + */ +// public function isStopped(int $campaignId): Json +// { +// try { +// $isStopped = $this->campaignService->isCampaignStopped($campaignId); +// return json(['stopped' => $isStopped], 200); +// } catch (ValidateException $e) { +// return json(['error' => $e->getMessage()], 400); +// } +// } +// + + /** + * 更新广告组的状态 + */ + public function updateGroupStatus(Request $request): Response + { + $adGroupStatus = [ + 0, // UNSPECIFIED + 1, // UNKNOWN + 2, // ENABLED + 3, // PAUSED + 4 // REMOVED + ]; + + $requestData = $request->all(); // 获取请求数据 + $requestData['refresh_token'] = $request->refresh_token; + $requestData['login_customer_id'] = $request->login_customer_id; +// dump($requestData); + + $status = $requestData['status']; + // $options['bid_micro_amount'] = $options['amount'] * 1000000 < 0 ? 0 : $options['amount'] * 1000000; + if (!in_array($status, $adGroupStatus)) { + return $this->errorResponse(101, 'status参数错误'); + } +// try { + $result = $this->googleAdsGroupService->updateGroupStatus($requestData); + if (!$result) { + return $this->errorResponse(101, 'Status update failed'); + } + return $this->successResponse(['message' => 'Status updated successfully'], $request); +// } catch (ValidateException $e) { +// return $this->errorResponse(400, $e->getMessage()); +// } + } + + /** + * 更新广告的状态 + */ + public function updateAdStatus(Request $request): Response + { + $adStatus = [ + 0, // UNSPECIFIED + 1, // UNKNOWN + 2, // ENABLED + 3, // PAUSED + 4 // REMOVED + ]; + + $requestData = $request->all(); // 获取请求数据 + $requestData['refresh_token'] = $request->refresh_token; + $requestData['login_customer_id'] = $request->login_customer_id; +// dump($requestData); + + $status = $requestData['status']; + if (!in_array($status, $adStatus)) { + return $this->errorResponse(101, 'status参数错误'); + } +// try { + $result = $this->googleAdsAdService->updateAdStatus($requestData); + if (!$result) { + return $this->errorResponse(101, 'Status update failed'); + } + return $this->successResponse(['message' => 'Status updated successfully'], $request); +// } catch (ValidateException $e) { +// return $this->errorResponse(400, $e->getMessage()); +// } + } + + + // 可以加入一些公共方法 + protected function successResponse($data, Request $request): Response + { + if ($request->jwtNewToken) { + return new Response(200, + [ + 'Content-Type' => 'application/json', + 'X-New-Token' => $request->jwtNewToken + ], + json_encode($data, JSON_UNESCAPED_UNICODE)); + } else { + return Json([ + 'code' => 0, + 'msg' => 'ok', + 'data' => $data, + ]); + } + + + } + + protected function errorResponse($code, $message, $data = []): Response + { + return Json([ + 'code' => $code, + 'msg' => $message ?: 'error', + 'data' => $data + ]); + } + +} diff --git a/app/model/BpsAdAd.php b/app/model/BpsAdAd.php new file mode 100644 index 0000000..1059b58 --- /dev/null +++ b/app/model/BpsAdAd.php @@ -0,0 +1,195 @@ + 'int', + 'ad_set_id' => 'int', + 'account_id' => 'int', + ]; + + // 设置json类型字段 + protected $json = ['metadata']; + + + // 默认值设置 + protected $defaults = [ + 'status' => 2, // 广告状态默认值为 'ENABLED' + 'platform_type' => 2, + ]; + + // 状态判断常量 + const STATUS_UNSPECIFIED = 0; + const STATUS_UNKNOWN = 1; + const STATUS_ENABLED = 2; + const STATUS_PAUSED = 3; + const STATUS_REMOVED = 4; + + // 获取广告状态 + public function getStatusTextAttr($value, $data) + { + $statusMap = [ + self::STATUS_UNSPECIFIED => 'UNSPECIFIED', + self::STATUS_UNKNOWN => 'UNKNOWN', + self::STATUS_ENABLED => 'ENABLED', + self::STATUS_PAUSED => 'PAUSED', + self::STATUS_REMOVED => 'REMOVED', + ]; + return $statusMap[$data['status']] ?? 'UNKNOWN'; + } + + // 定义广告类型枚举常量 + const UNSPECIFIED = 0; + const UNKNOWN = 1; + const TEXT_AD = 2; + const EXPANDED_TEXT_AD = 3; + const EXPANDED_DYNAMIC_SEARCH_AD = 7; + const HOTEL_AD = 8; + const SHOPPING_SMART_AD = 9; + const SHOPPING_PRODUCT_AD = 10; + const VIDEO_AD = 12; + const IMAGE_AD = 14; + const RESPONSIVE_SEARCH_AD = 15; + const LEGACY_RESPONSIVE_DISPLAY_AD = 16; + const APP_AD = 17; + const LEGACY_APP_INSTALL_AD = 18; + const RESPONSIVE_DISPLAY_AD = 19; + const LOCAL_AD = 20; + const HTML5_UPLOAD_AD = 21; + const DYNAMIC_HTML5_AD = 22; + const APP_ENGAGEMENT_AD = 23; + const SHOPPING_COMPARISON_LISTING_AD = 24; + const VIDEO_BUMPER_AD = 25; + const VIDEO_NON_SKIPPABLE_IN_STREAM_AD = 26; + const VIDEO_OUTSTREAM_AD = 27; + const VIDEO_TRUEVIEW_IN_STREAM_AD = 29; + const VIDEO_RESPONSIVE_AD = 30; + const SMART_CAMPAIGN_AD = 31; + const CALL_AD = 32; + const APP_PRE_REGISTRATION_AD = 33; + const IN_FEED_VIDEO_AD = 34; + const DEMAND_GEN_MULTI_ASSET_AD = 40; + const DEMAND_GEN_CAROUSEL_AD = 41; + const TRAVEL_AD = 37; + const DEMAND_GEN_VIDEO_RESPONSIVE_AD = 42; + const DEMAND_GEN_PRODUCT_AD = 39; + + private static $valueToName = [ + self::UNSPECIFIED => 'UNSPECIFIED', + self::UNKNOWN => 'UNKNOWN', + self::TEXT_AD => 'TEXT_AD', + self::EXPANDED_TEXT_AD => 'EXPANDED_TEXT_AD', + self::EXPANDED_DYNAMIC_SEARCH_AD => 'EXPANDED_DYNAMIC_SEARCH_AD', + self::HOTEL_AD => 'HOTEL_AD', + self::SHOPPING_SMART_AD => 'SHOPPING_SMART_AD', + self::SHOPPING_PRODUCT_AD => 'SHOPPING_PRODUCT_AD', + self::VIDEO_AD => 'VIDEO_AD', + self::IMAGE_AD => 'IMAGE_AD', + self::RESPONSIVE_SEARCH_AD => 'RESPONSIVE_SEARCH_AD', + self::LEGACY_RESPONSIVE_DISPLAY_AD => 'LEGACY_RESPONSIVE_DISPLAY_AD', + self::APP_AD => 'APP_AD', + self::LEGACY_APP_INSTALL_AD => 'LEGACY_APP_INSTALL_AD', + self::RESPONSIVE_DISPLAY_AD => 'RESPONSIVE_DISPLAY_AD', + self::LOCAL_AD => 'LOCAL_AD', + self::HTML5_UPLOAD_AD => 'HTML5_UPLOAD_AD', + self::DYNAMIC_HTML5_AD => 'DYNAMIC_HTML5_AD', + self::APP_ENGAGEMENT_AD => 'APP_ENGAGEMENT_AD', + self::SHOPPING_COMPARISON_LISTING_AD => 'SHOPPING_COMPARISON_LISTING_AD', + self::VIDEO_BUMPER_AD => 'VIDEO_BUMPER_AD', + self::VIDEO_NON_SKIPPABLE_IN_STREAM_AD => 'VIDEO_NON_SKIPPABLE_IN_STREAM_AD', + self::VIDEO_OUTSTREAM_AD => 'VIDEO_OUTSTREAM_AD', + self::VIDEO_TRUEVIEW_IN_STREAM_AD => 'VIDEO_TRUEVIEW_IN_STREAM_AD', + self::VIDEO_RESPONSIVE_AD => 'VIDEO_RESPONSIVE_AD', + self::SMART_CAMPAIGN_AD => 'SMART_CAMPAIGN_AD', + self::CALL_AD => 'CALL_AD', + self::APP_PRE_REGISTRATION_AD => 'APP_PRE_REGISTRATION_AD', + self::IN_FEED_VIDEO_AD => 'IN_FEED_VIDEO_AD', + self::DEMAND_GEN_MULTI_ASSET_AD => 'DEMAND_GEN_MULTI_ASSET_AD', + self::DEMAND_GEN_CAROUSEL_AD => 'DEMAND_GEN_CAROUSEL_AD', + self::TRAVEL_AD => 'TRAVEL_AD', + self::DEMAND_GEN_VIDEO_RESPONSIVE_AD => 'DEMAND_GEN_VIDEO_RESPONSIVE_AD', + self::DEMAND_GEN_PRODUCT_AD => 'DEMAND_GEN_PRODUCT_AD', + ]; + + // 检查广告类型是否有效 + public static function isValidAdType($adType) { + return in_array($adType, array_keys(self::$valueToName)); + } + + // 获取广告类型名称 + public static function getAdTypeName($adType) { + return self::$valueToName[$adType] ?? 'UNKNOWN'; + } + + // 在模型中使用这些常量 + public function setAdType($adType) { + if (!self::isValidAdType($adType)) { + throw new Exception("Invalid ad type"); + } + $this->ad_type = $adType; + } + + // 更新广告状态 +// public function updateStatus($status) +// { +// if (!in_array($status, [self::STATUS_ENABLED, self::STATUS_PAUSED, self::STATUS_REMOVED])) { +//// throw new \think\exception\ValidateException('Invalid status'); +// } +// +// $this->status = $status; +// return $this->save(); +// } + + // 判断广告状态 + public function isEnabled() + { + return $this->status == self::STATUS_ENABLED; + } + + public function isPaused() + { + return $this->status == self::STATUS_PAUSED; + } + + public function isRemoved() + { + return $this->status == self::STATUS_REMOVED; + } + + // 关联 AdGroup 模型(广告属于广告组) + // 即使没有外键约束,依然可以使用 belongsTo 访问 AdGroup 数据 + public function adSet() + { + return $this->belongsTo(BpsAdSet::class, 'ad_set_id', 'ad_set_id'); + } + + // 关联到素材关系 +// public function assetRelations() +// { +// return $this->hasMany(AssetRelation::class, 'ad_id', 'ad_id'); +// } + + // 关联 Customer 模型(广告属于客户) +// public function customer() +// { +// return $this->belongsTo(GoogleAdsCustomer::class, 'customer_id', 'customer_id'); +// } +} \ No newline at end of file diff --git a/app/model/BpsAdCampaign.php b/app/model/BpsAdCampaign.php new file mode 100644 index 0000000..a184590 --- /dev/null +++ b/app/model/BpsAdCampaign.php @@ -0,0 +1,118 @@ + 'int', +// 'account_id' => 'int', +// 'budget_amount_micros' => 'int', +// ]; + + // 默认值设置 + protected $defaults = [ + 'status' => 'ENABLED', // 活动状态默认值为 'ENABLED' +// 'advertising_channel_type' => 'SEARCH', // 广告渠道类型默认值为 'SEARCH' + 'platform_type' => 2, // 广告渠道类型默认值为 'SEARCH' + ]; + + + // 状态判断常量 + const STATUS_UNSPECIFIED = 0; + const STATUS_UNKNOWN = 1; + const STATUS_ENABLED = 2; + const STATUS_PAUSED = 3; + const STATUS_REMOVED = 4; + + // 获取活动状态 + public function getStatusTextAttr($value, $data) + { + $statusMap = [ + self::STATUS_UNSPECIFIED => 'UNSPECIFIED', + self::STATUS_UNKNOWN => 'UNKNOWN', + self::STATUS_ENABLED => 'ENABLED', + self::STATUS_PAUSED => 'PAUSED', + self::STATUS_REMOVED => 'REMOVED', + ]; + return $statusMap[$data['status']] ?? 'UNKNOWN'; + } + + // 更新状态方法 +// public function updateStatus($status) +// { +// if (!in_array($status, [self::STATUS_ENABLED, self::STATUS_PAUSED, self::STATUS_REMOVED])) { +//// throw new \think\exception\ValidateException('Invalid status'); +// } +// $this->status = $status; +// return $this->save(); +// } + + + + // 判断当前活动状态 + public function isEnabled() + { + return $this->status == self::STATUS_ENABLED; + } + + public function isPaused() + { + return $this->status == self::STATUS_PAUSED; + } + + public function isRemoved() + { + return $this->status == self::STATUS_REMOVED; + } + + + // 关联 Customer 模型(广告活动属于客户) +// public function customer() +// { +// return $this->belongsTo(GoogleAdsCustomer::class, 'customer_id', 'customer_id'); +// } + + // 关联 AdGroup 模型(一个广告活动下有多个广告组) + public function adGroups() + { + return $this->hasMany(AdGroup::class, 'campaign_id', 'campaign_id'); + } + + // 关联 Ad 模型(一个广告活动下有多个广告) + public function ads() + { + return $this->hasManyThrough(BpsAdAd::class, BpsAdSet::class, 'campaign_id', 'ad_set_id', 'campaign_id', 'ad_set_id'); + } + + // 关联到广告数据表 +// public function adDayData() +// { +// return $this->hasMany(DayData::class, 'campaign_id', 'campaign_id'); +// } + public function adDayData() + { + return $this->hasMany(DayData::class, 'campaign_id', 'campaign_id'); + } + // 关联到素材关系 + public function assetRelations() + { + return $this->hasMany(AssetRelation::class, 'campaign_id', 'campaign_id'); + } +} \ No newline at end of file diff --git a/app/model/BpsAdInsight.php b/app/model/BpsAdInsight.php new file mode 100644 index 0000000..c1cf61a --- /dev/null +++ b/app/model/BpsAdInsight.php @@ -0,0 +1,93 @@ + 'int', + 'account_id' => 'int', + 'clicks' => 'int', + 'spend' => 'int', + 'purchases_value' => 'int', + 'revenue' => 'int', + 'impressions' => 'int', + 'date' => 'date', + ]; + + // 默认值设置 + protected $defaults = [ + 'clicks' => 0, + 'spend' => 0, + 'purchases_value' => 0, + 'revenue ' => 0, + 'impressions' => 0, + 'ad_name' => '', + ]; + + // 检查唯一键 + protected function checkUniqueKey() + { + $exists = $this->where('ad_id', $this->ad_id) + ->where('date', $this->date) + ->where('platform', $this->platform) + ->find(); + + if ($exists && $exists->id != $this->id) { + throw new \Exception('数据已存在,ad_id、date 和 platform 必须唯一'); + } + } + + // 在保存数据前调用 + protected static function onBeforeWrite($model) + { + $model->checkUniqueKey(); + } + + + // 关联 Campaign 模型(报告数据属于广告活动) + public function campaign() + { + return $this->belongsTo(BpsAdCampaign::class, 'campaign_id', 'campaign_id'); + } + + // 关联 AdGroup 模型(报告数据属于广告组) + public function adSet() + { + return $this->belongsTo(BpsAdSet::class, 'ad_set_id', 'ad_set_id'); + } + + // 关联 Ad 模型(报告数据属于广告) + public function ad() + { + return $this->belongsTo(BpsAdAd::class, 'ad_id', 'ad_id'); + } + + // 关联 Customer 模型(报告数据与客户相关) +// public function customer() +// { +// return $this->belongsTo(GoogleAdsCustomer::class, 'account_id', 'account_id'); +// } + + // 关联 Budget 模型(报告数据与预算相关) +// public function budget() +// { +// return $this->belongsTo(CampaignBudget::class, 'budget_id', 'budget_id'); +// } +} \ No newline at end of file diff --git a/app/model/BpsAdSet.php b/app/model/BpsAdSet.php new file mode 100644 index 0000000..3a72de1 --- /dev/null +++ b/app/model/BpsAdSet.php @@ -0,0 +1,106 @@ + 'int', +// 'campaign_id' => 'int', +// 'account_id' => 'int', +// ]; + + // 默认值设置 + protected $defaults = [ + 'platform_type' => 2, + ]; + + // 状态判断常量 + const STATUS_UNSPECIFIED = 0; + const STATUS_UNKNOWN = 1; + const STATUS_ENABLED = 2; + const STATUS_PAUSED = 3; + const STATUS_REMOVED = 4; + + // 获取广告组状态 + public function getStatusTextAttr($value, $data) + { + $statusMap = [ + self::STATUS_UNSPECIFIED => 'UNSPECIFIED', + self::STATUS_UNKNOWN => 'UNKNOWN', + self::STATUS_ENABLED => 'ENABLED', + self::STATUS_PAUSED => 'PAUSED', + self::STATUS_REMOVED => 'REMOVED', + ]; + return $statusMap[$data['status']] ?? 'UNKNOWN'; + } + + // 更新广告组状态 +// public function updateStatus($status) +// { +// if (!in_array($status, [self::STATUS_ENABLED, self::STATUS_PAUSED, self::STATUS_REMOVED])) { +//// throw new \think\exception\ValidateException('Invalid status'); +// } +// +// $this->status = $status; +// return $this->save(); +// } + + // 判断广告组状态 + public function isEnabled() + { + return $this->status == self::STATUS_ENABLED; + } + + public function isPaused() + { + return $this->status == self::STATUS_PAUSED; + } + + public function isRemoved() + { + return $this->status == self::STATUS_REMOVED; + } + + + // 关联 Campaign 模型(广告组属于广告活动) + public function campaign() + { + return $this->belongsTo(BpsAdCampaign::class, 'campaign_id', 'campaign_id'); + } + + // 关联 Customer 模型(广告组属于客户) +// public function customer() +// { +// return $this->belongsTo(GoogleAdsCustomer::class, 'customer_id', 'customer_id'); +// } + + // 关联 Ad 模型(广告组包含多个广告) + public function ads() + { + return $this->hasMany(BpsAdAd::class, 'ad_set_id', 'ad_set_id'); + } + + // 关联到素材关系 +// public function assetRelations() +// { +// return $this->hasMany(AssetRelation::class, 'ad_group_id', 'ad_group_id'); +// } +} + diff --git a/app/service/AdsInsightService.php b/app/service/AdsInsightService.php new file mode 100644 index 0000000..4739ef8 --- /dev/null +++ b/app/service/AdsInsightService.php @@ -0,0 +1,995 @@ + 'ENABLE', // 2 代表广告已启用 + 3 => 'PAUSED', // 3 代表广告待审核等状态 + // 其他状态可以继续添加 + ]; + + /** + * 获取广告系列列表 + */ + public static function getCampaignList($platformType, $customerIds, $page, $pageSize, $keyword, $startDate = null, $endDate = null) + { + // 检查 customerIds 是否为空,直接返回空结构 + if (empty($customerIds)) { + return [ + 'pagination' => [ + 'startIndex' => 0, + 'maxResults' => $pageSize, + 'count' => 0, + 'pageNo' => $page, + 'pageSize' => $pageSize, + 'pages' => 0, + ], + 'statistics' => [ + ], + 'data' => [], + ]; + } + // 动态构建日期条件 + $dateCondition = ''; + if ($startDate && $endDate) { + $dateCondition = "d.date BETWEEN '{$startDate}' AND '{$endDate}'"; + } else { + } + + // 基础查询:广告活动和日数据表联接 + $query = BpsAdCampaign::alias('c') + ->cache(false) // 强制不使用缓存 + ->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, + COALESCE(SUM(d.assisted_purchases), 0) as assisted_purchases, + COALESCE(SUM(d.last_clicked_purchases), 0) as last_clicked_purchases, + 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.cost_per_atc), 0) as cost_per_atc, + COALESCE(SUM(d.purchases), 0) as purchases, + COALESCE(SUM(d.purchases_value), 0) as purchases_value, + COALESCE(SUM(d.revenue), 0) as revenue, + COALESCE(SUM(d.total_cost), 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') + ->where('c.account_id', 'in', $customerIds); // 添加 customerIds 条件 + + // 添加关键字过滤条件 + $query->where(function ($query) use ($keyword, $platformType) { + if ($keyword) { + $query->where('c.campaign_name', 'like', '%' . $keyword . '%'); + } + if ($platformType) { + $platformType = (int)$platformType; + $query->where('c.platform_type', '=', $platformType); + } + }); + + // 获取所有符合条件的数据(不分页) + $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')); + // 汇总统计数据 + $statistics = [ + 'assisted_purchases' => array_sum(array_column($allCampaigns, 'assisted_purchases')), + 'last_clicked_purchases' => array_sum(array_column($allCampaigns, 'last_clicked_purchases')), + 'roas' => $total_spend === 0 ? '-' : round($total_purchases_value / $total_spend, 2), + 'amount_spend' => '$' . number_format($total_spend, 2) ?: '$0.00', // 格式化支出 + 'clicks' => $total_clicks, + 'impressions' => $total_impressions, + 'adds_to_cart' => array_sum(array_column($allCampaigns, 'adds_to_cart')), + 'cost_per_atc' => array_sum(array_column($allCampaigns, 'cost_per_atc')), + 'purchases' => array_sum(array_column($allCampaigns, 'purchases')), + 'purchases_value' => array_sum(array_column($allCampaigns, 'purchases_value')), + 'revenue' => '$' . number_format(array_sum(array_column($allCampaigns, 'revenue')), 2) ?: '$0.00', // 格式化收入 + 'total_cost' => '$' . number_format($total_cost, 2) ?: '$0.00', // 格式化总成本 + 'conversion_rate' => '-', // 没有计算逻辑,保持为 '-' + 'net_profit' => '-', // 没有计算 net_profit,保持为 '-' + 'net_profit_margin' => '-', // 没有计算 net_profit_margin,保持为 '-' + 'net_profit_on_ad_spend' => '-', // 没有计算 net_profit_on_ad_spend,保持为 '-' + // 计算总的 CTR + 'ctr' => ($total_impressions > 0) ? number_format(($total_clicks / $total_impressions) * 100, 2) . '%' : '-', // 格式化为百分比 + ]; + + // 获取分页数据 + $campaigns = $query->paginate($pageSize, false, ['page' => $page]); + + // 确保数据格式统一 + $result = array_map(function ($item) { + $ctr = $item['impressions'] > 0 ? number_format(($item['clicks'] / $item['impressions']) * 100, 2) . '%' : '-'; // 格式化为百分比 + return [ + 'id' => $item['campaign_id'], + 'account_id' => $item['account_id'], // 映射为 customer_id + 'name' => $item['name'] ?: '-', // 若 name 为空则显示 '-' + 'status' => $item['status'], + 'assisted_purchases' => $item['assisted_purchases'], + 'last_clicked_purchases' => $item['last_clicked_purchases'], + 'roas' => $item['spend'] == 0 ? '-' : round($item['purchases_value'] / $item['spend'], 2), + 'spend' => '$' . number_format($item['spend'], 2), // 格式化支出 + 'impressions' => $item['impressions'], + 'clicks' => $item['clicks'], + 'ctr' => $ctr, // CTR 字段 + 'adds_to_cart' => $item['adds_to_cart'], + 'cost_per_atc' => $item['cost_per_atc'], + 'purchases' => $item['purchases'], + 'purchases_value' => '$' . number_format($item['purchases_value'], 2), // 格式化购买金额 + 'revenue' => '$' . number_format($item['revenue'], 2), // 格式化收入 + 'total_cost' => '$' . number_format($item['total_cost'], 2), // 格式化总成本 + 'conversion_rate' => '-', // 没有提供有效的计算,保持为 '-' + 'net_profit' => '-', // 没有提供 net_profit 计算,保持为 '-' + 'net_profit_margin' => '-', // 没有提供 net_profit_margin 计算,保持为 '-' + 'net_profit_on_ad_spend' => '-', // 没有提供 net_profit_on_ad_spend 计算,保持为 '-' + ]; + }, $campaigns->items()); + + // Pagination 数据 + $pagination = [ + 'startIndex' => ($page - 1) * $pageSize, + 'maxResults' => $pageSize, + 'count' => $campaigns->total(), + 'pageNo' => $campaigns->currentPage(), + 'pageSize' => $pageSize, + 'pages' => $campaigns->lastPage(), + ]; + + return [ + 'pagination' => $pagination, + 'statistics' => $statistics, + 'data' => $result, + ]; + } + + /** + * 获取广告组列表 + */ + public static function getAdsetList($platformType, $customerIds, $page, $pageSize, $keyword, $startDate = null, $endDate = null) + { + // 检查 customerIds 是否为空,直接返回空结构 + if (empty($customerIds)) { + return [ + 'pagination' => [ + 'startIndex' => 0, + 'maxResults' => $pageSize, + 'count' => 0, + 'pageNo' => $page, + 'pageSize' => $pageSize, + 'pages' => 0, + ], + 'statistics' => [ + ], + 'data' => [], + ]; + } + + // 动态构建日期条件 + $dateCondition = ''; + if ($startDate && $endDate) { + $dateCondition = "d.date BETWEEN '{$startDate}' AND '{$endDate}'"; + } + + // 基础查询:广告组和日数据表联接 + $query = BpsAdSet::alias('s') + ->cache(false) // 强制不使用缓存 + ->leftJoin('bps.bps_ads_insights d', "s.ad_set_id = d.ad_set_id AND s.platform_type = d.platform AND {$dateCondition}") + ->field('s.ad_set_id, s.status as status, s.name, s.account_id, + COALESCE(SUM(d.assisted_purchases), 0) as assisted_purchases, + COALESCE(SUM(d.last_clicked_purchases), 0) as last_clicked_purchases, + 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.cost_per_atc), 0) as cost_per_atc, + COALESCE(SUM(d.purchases), 0) as purchases, + COALESCE(SUM(d.purchases_value), 0) as purchases_value, + COALESCE(SUM(d.revenue), 0) as revenue, + COALESCE(SUM(d.total_cost), 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') + ->where('s.account_id', 'in', $customerIds); // 添加 customerIds 条件 + + // 添加关键字过滤条件 + $query->where(function ($query) use ($keyword, $platformType) { + if ($keyword) { + $query->where('s.name', 'like', '%' . $keyword . '%'); + } + if ($platformType) { + $platformType = (int)$platformType; + $query->where('s.platform_type', '=', $platformType); + } + }); + + // 获取所有符合条件的数据(不分页) + $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')); + + // 汇总统计数据 + $statistics = [ + 'assisted_purchases' => array_sum(array_column($allAdsets, 'assisted_purchases')), + 'last_clicked_purchases' => array_sum(array_column($allAdsets, 'last_clicked_purchases')), + 'roas' => $total_spend === 0 ? '-' : round($total_purchases_value / $total_spend, 2), + 'amount_spend' => '$' . number_format($total_spend, 2) ?: '$0.00', // 格式化支出 + 'clicks' => $total_clicks, + 'impressions' => array_sum(array_column($allAdsets, 'impressions')), + 'adds_to_cart' => array_sum(array_column($allAdsets, 'adds_to_cart')), + 'cost_per_atc' => array_sum(array_column($allAdsets, 'cost_per_atc')), + 'purchases' => array_sum(array_column($allAdsets, 'purchases')), + 'purchases_value' => array_sum(array_column($allAdsets, 'purchases_value')), + 'revenue' => '$' . number_format(array_sum(array_column($allAdsets, 'revenue')), 2) ?: '$0.00', // 格式化收入 + 'total_cost' => '$' . number_format($total_cost, 2) ?: '$0.00', // 格式化总成本 + 'conversion_rate' => '-', // 没有计算逻辑,保持为 '-' + 'net_profit' => '-', // 没有计算 net_profit,保持为 '-' + 'net_profit_margin' => '-', // 没有计算 net_profit_margin,保持为 '-' + 'net_profit_on_ad_spend' => '-', // 没有计算 net_profit_on_ad_spend,保持为 '-' + // 计算总的 CTR + 'ctr' => ($total_impressions > 0) ? number_format(($total_clicks / $total_impressions) * 100, 2) . '%' : '-', // 格式化为百分比 + ]; + + // 获取分页数据 + $adsets = $query->paginate($pageSize, false, ['page' => $page]); + + // 确保数据格式统一 + $result = array_map(function ($item) { + $ctr = $item['impressions'] > 0 ? number_format(($item['clicks'] / $item['impressions']) * 100, 2) . '%' : '-'; // 格式化为百分比 + return [ + 'id' => $item['ad_set_id'], + 'account_id' => $item['account_id'], // 映射为 customer_id + 'name' => $item['name'] ?: '-', // 若 name 为空则显示 '-' + 'status' => $item['status'], + 'assisted_purchases' => $item['assisted_purchases'], + 'last_clicked_purchases' => $item['last_clicked_purchases'], + 'roas' => $item['spend'] == 0 ? '-' : round($item['purchases_value'] / $item['spend'], 2), + 'spend' => '$' . number_format($item['spend'], 2), // 格式化支出 + 'impressions' => $item['impressions'], + 'clicks' => $item['clicks'], + 'ctr' => $ctr, // CTR 字段 + 'adds_to_cart' => $item['adds_to_cart'], + 'cost_per_atc' => $item['cost_per_atc'], + 'purchases' => $item['purchases'], + 'purchases_value' => '$' . number_format($item['purchases_value'], 2), // 格式化购买金额 + 'revenue' => '$' . number_format($item['revenue'], 2), // 格式化收入 + 'total_cost' => '$' . number_format($item['total_cost'], 2), // 格式化总成本 + 'conversion_rate' => '-', // 没有提供有效的计算,保持为 '-' + 'net_profit' => '-', // 没有提供 net_profit 计算,保持为 '-' + 'net_profit_margin' => '-', // 没有提供 net_profit_margin 计算,保持为 '-' + 'net_profit_on_ad_spend' => '-', // 没有提供 net_profit_on_ad_spend 计算,保持为 '-' + ]; + }, $adsets->items()); + + // Pagination 数据 + $pagination = [ + 'startIndex' => ($page - 1) * $pageSize, + 'maxResults' => $pageSize, + 'count' => $adsets->total(), + 'pageNo' => $adsets->currentPage(), + 'pageSize' => $pageSize, + 'pages' => $adsets->lastPage(), + ]; + + return [ + 'pagination' => $pagination, + 'statistics' => $statistics, + 'data' => $result, + ]; + } + + public static function getAdList($platformType, $customerIds, $page, $pageSize, $keyword, $startDate = null, $endDate = null) + { + // 检查 customerIds 是否为空,直接返回空结构 + if (empty($customerIds)) { + return [ + 'pagination' => [ + 'startIndex' => 0, + 'maxResults' => $pageSize, + 'count' => 0, + 'pageNo' => $page, + 'pageSize' => $pageSize, + 'pages' => 0, + ], + 'statistics' => [], + 'data' => [], + ]; + } + + // 动态构建日期条件 + $dateCondition = ''; + if ($startDate && $endDate) { + $dateCondition = "d.date BETWEEN '{$startDate}' AND '{$endDate}'"; + } + + // 基础查询:广告和日数据表联接 + $query = BpsAdAd::alias('a') + ->cache(false) // 强制不使用缓存 + ->leftJoin('bps.bps_ads_insights d', "a.ad_id = d.ad_id AND a.platform_type = d.platform AND {$dateCondition}") + ->field('a.ad_id, a.status as status, a.name, a.account_id, + COALESCE(SUM(d.assisted_purchases), 0) as assisted_purchases, + COALESCE(SUM(d.last_clicked_purchases), 0) as last_clicked_purchases, + 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.cost_per_atc), 0) as cost_per_atc, + COALESCE(SUM(d.purchases), 0) as purchases, + COALESCE(SUM(d.purchases_value), 0) as purchases_value, + COALESCE(SUM(d.revenue), 0) as revenue, + COALESCE(SUM(d.total_cost), 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') + ->where('a.account_id', 'in', $customerIds); // 添加 customerIds 条件 + + // 添加关键字过滤条件 + $query->where(function ($query) use ($keyword, $platformType) { + if ($keyword) { + $query->where('a.name', 'like', '%' . $keyword . '%'); + } + if ($platformType) { + $platformType = (int)$platformType; + $query->where('a.platform_type', '=', $platformType); + } + }); + + // 获取所有符合条件的数据(不分页) + $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')); + + $statistics = [ + 'assisted_purchases' => array_sum(array_column($allAds, 'assisted_purchases')), + 'last_clicked_purchases' => array_sum(array_column($allAds, 'last_clicked_purchases')), + 'roas' => $total_spend === 0 ? '-' : round($total_purchases_value / $total_spend, 2), + 'amount_spend' => '$' . number_format($total_spend, 2) ?: '$0.00', // 格式化支出 + 'clicks' => $total_clicks, + 'impressions' => array_sum(array_column($allAds, 'impressions')), + 'adds_to_cart' => array_sum(array_column($allAds, 'adds_to_cart')), + 'cost_per_atc' => array_sum(array_column($allAds, 'cost_per_atc')), + 'purchases' => array_sum(array_column($allAds, 'purchases')), + 'purchases_value' => array_sum(array_column($allAds, 'purchases_value')), + 'revenue' => '$' . number_format(array_sum(array_column($allAds, 'revenue')), 2) ?: '$0.00', // 格式化收入 + 'total_cost' => '$' . number_format($total_cost, 2) ?: '$0.00', // 格式化总成本 + 'conversion_rate' => '-', // 没有计算逻辑,保持为 '-' + 'net_profit' => '-', // 没有计算 net_profit,保持为 '-' + 'net_profit_margin' => '-', // 没有计算 net_profit_margin,保持为 '-' + 'net_profit_on_ad_spend' => '-', // 没有计算 net_profit_on_ad_spend,保持为 '-' + 'ctr' => ($total_impressions > 0) ? number_format(($total_clicks / $total_impressions) * 100, 2) . '%' : '-', // 格式化为百分比 + ]; + + // 获取分页数据 + $ads = $query->paginate($pageSize, false, ['page' => $page]); + + // 确保数据格式统一 + $result = array_map(function ($item) { + $ctr = $item['impressions'] > 0 ? number_format(($item['clicks'] / $item['impressions']) * 100, 2) . '%' : '-'; // 格式化为百分比 + return [ + 'id' => $item['ad_id'], + 'account_id' => $item['account_id'], // 映射为 customer_id + 'name' => $item['name'] ?: '-', // 若 name 为空则显示 '-' + 'status' => $item['status'], + 'assisted_purchases' => $item['assisted_purchases'], + 'last_clicked_purchases' => $item['last_clicked_purchases'], + 'roas' => $item['spend'] == 0 ? '-' : round($item['purchases_value'] / $item['spend'], 2), + 'spend' => '$' . number_format($item['spend'], 2), // 格式化支出 + 'impressions' => $item['impressions'], + 'clicks' => $item['clicks'], + 'ctr' => $ctr, // CTR 字段 + 'adds_to_cart' => $item['adds_to_cart'], + 'cost_per_atc' => $item['cost_per_atc'], + 'purchases' => $item['purchases'], + 'purchases_value' => '$' . number_format($item['purchases_value'], 2), // 格式化购买金额 + 'revenue' => '$' . number_format($item['revenue'], 2), // 格式化收入 + 'total_cost' => '$' . number_format($item['total_cost'], 2), // 格式化总成本 + 'conversion_rate' => '-', // 没有提供有效的计算,保持为 '-' + 'net_profit' => '-', // 没有提供 net_profit 计算,保持为 '-' + 'net_profit_margin' => '-', // 没有提供 net_profit_margin 计算,保持为 '-' + 'net_profit_on_ad_spend' => '-', // 没有提供 net_profit_on_ad_spend 计算,保持为 '-' + ]; + }, $ads->items()); + + // Pagination 数据 + $pagination = [ + 'startIndex' => ($page - 1) * $pageSize, + 'maxResults' => $pageSize, + 'count' => $ads->total(), + 'pageNo' => $ads->currentPage(), + 'pageSize' => $pageSize, + 'pages' => $ads->lastPage(), + ]; + + return [ + 'pagination' => $pagination, + 'statistics' => $statistics, + 'data' => $result, + ]; + } + + public static function getThirdUserList($platformType, $userIds, $page, $pageSize, $keyword, $startDate = null, $endDate = null) + { + // 检查 userIds 是否为空,直接返回空结构 + if (empty($userIds)) { + return [ + 'pagination' => [ + 'startIndex' => 0, + 'maxResults' => $pageSize, + 'count' => 0, + 'pageNo' => $page, + 'pageSize' => $pageSize, + 'pages' => 0, + ], + 'statistics' => [], + 'data' => [], + ]; + } +// dump($userIds); + // 动态构建日期条件 + $dateCondition = '1=1'; // 默认没有日期限制 + if ($startDate && $endDate) { + $dateCondition = "d.date BETWEEN '{$startDate}' AND '{$endDate}'"; + } +// u.user_id as third_user_id, u.third_type, u.facebook_user_id, +// a.advertiser_id, a.advertiser_name, a.google_manager, a.google_test_account, + // 基础查询:第三方用户和广告商数据表联接 + $query = ThirdUser::alias('u') + ->cache(false) // 强制不使用缓存 + ->leftJoin('bps.bps_third_user_advertiser a', "u.id = a.doc_") + ->field('u.id as user_id,u.third_type,a.advertiser_name, + + COALESCE(SUM(d.assisted_purchases), 0) as assisted_purchases, + COALESCE(SUM(d.last_clicked_purchases), 0) as last_clicked_purchases, + COALESCE(SUM(d.spend) / 1000000, 0) as spend, + COALESCE(SUM(d.impressions), 0) as impressions, + COALESCE(SUM(d.clicks), 0) as clicks, + COALESCE(SUM(d.adds_to_cart), 0) as adds_to_cart, + COALESCE(SUM(d.cost_per_atc), 0) as cost_per_atc, + COALESCE(SUM(d.purchases), 0) as purchases, + COALESCE(SUM(d.purchases_value), 0) as purchases_value, + COALESCE(SUM(d.revenue), 0) as revenue, + COALESCE(SUM(d.total_cost), 0) as total_cost') + ->leftJoin('bps.bps_ads_insights d', "a.advertiser_id = d.account_id AND {$dateCondition}") + ->where('u.id', 'in', $userIds); // 添加 userIds 条件 + + // 添加关键字过滤条件 + $query->where(function ($query) use ($keyword, $platformType) { + if ($keyword) { + $query->where('a.advertiser_name', 'like', '%' . $keyword . '%'); + } + if ($platformType) { + $query->where('d.platform', '=', $platformType); + } + }); + + // 添加 GROUP BY 子句 +// $query->group('u.id, u.user_id, u.third_type, u.facebook_user_id, +// a.advertiser_id, a.advertiser_name, a.google_manager, a.google_test_account'); + $query->group('u.id,u.third_type,a.advertiser_name'); + + // 调试打印 SQL + + + // 获取所有符合条件的数据(不分页) + $allUsers = $query->select()->toArray(); +// dump($query->getLastSql()); +// +// dump($allUsers); + + // 汇总统计数据 + $total_spend = array_sum(array_column($allUsers, 'spend')); + $total_impressions = array_sum(array_column($allUsers, 'impressions')); + $total_clicks = array_sum(array_column($allUsers, 'clicks')); + $total_adds_to_cart = array_sum(array_column($allUsers, 'adds_to_cart')); + $total_cost_per_atc = array_sum(array_column($allUsers, 'cost_per_atc')); + $total_purchases = array_sum(array_column($allUsers, 'purchases')); + $total_purchases_value = array_sum(array_column($allUsers, 'purchases_value')); + $total_revenue = array_sum(array_column($allUsers, 'revenue')); + $total_cost = array_sum(array_column($allUsers, 'total_cost')); + + // 计算 ROAS + $roas = $total_spend === 0 ? '-' : round($total_purchases_value / $total_spend, 2); + + // 计算 CTR + $ctr = $total_impressions > 0 ? number_format(($total_clicks / $total_impressions) * 100, 2) . '%' : '-'; + + // 汇总统计字段初始化 + $statistics = [ + 'assisted_purchases' => 0, + 'last_clicked_purchases' => 0, + 'roas' => $total_spend === 0 ? '-' : round($total_purchases_value / $total_spend, 2), + 'amount_spend' => '$' . number_format($total_spend, 2) ?: '$0.00', // 格式化支出 + 'clicks' => $total_clicks, + 'impressions' => $total_impressions, + 'adds_to_cart' => $total_adds_to_cart, + 'cost_per_atc' => $total_cost_per_atc, + 'purchases' => $total_purchases, + 'purchases_value' => '$' . number_format($total_purchases_value, 2) ?: '$0.00', // 格式化销售额$total_purchases_value, + 'revenue' => '$' . number_format($total_revenue, 2) ?: '$0.00', // 格式化收入 + 'total_cost' => '$' . number_format($total_cost, 2) ?: '$0.00', // 格式化总成本 + 'conversion_rate' => '-', // 没有计算逻辑,保持为 '-' + 'net_profit' => '-', // 没有提供 net_profit,保持为 '-' + 'net_profit_margin' => '-', // 没有提供 net_profit_margin,保持为 '-' + 'net_profit_on_ad_spend' => '-', // 没有提供 net_profit_on_ad_spend,保持为 '-' + 'ctr' => $ctr, // CTR 字段 + ]; + + // 获取分页数据 + $users = $query->paginate($pageSize, false, ['page' => $page]); + + // 按照 third_user_id 聚合统计字段 + $aggregatedUsers = []; + +// 遍历每个用户的统计数据 + foreach ($users->items() as $item) { + $thirdUserId = $item['user_id']; + + if (!isset($aggregatedUsers[$thirdUserId])) { + $aggregatedUsers[$thirdUserId] = [ + 'user_id' => $item['user_id'], + 'third_type' => $item['third_type'], + 'advertiser_name' => $item['advertiser_name'], + 'assisted_purchases' => 0, + 'last_clicked_purchases' => 0, + 'roas' => 0, + 'spend' => 0, + 'impressions' => 0, + 'clicks' => 0, + 'adds_to_cart' => 0, + 'cost_per_atc' => 0, + 'purchases' => 0, + 'purchases_value' => 0, + 'revenue' => 0, + 'total_cost' => 0, + 'net_profit' => '-', // 没有提供 net_profit,保持为 '-' + 'net_profit_margin' => '-', // 没有提供 net_profit_margin,保持为 '-' + 'net_profit_on_ad_spend' => '-', // 没有提供 net_profit_on_ad_spend,保持为 '-' + + ]; + } + + // 汇总统计 + $aggregatedUsers[$thirdUserId]['spend'] += $item['spend']; + $aggregatedUsers[$thirdUserId]['impressions'] += $item['impressions']; + $aggregatedUsers[$thirdUserId]['clicks'] += $item['clicks']; + $aggregatedUsers[$thirdUserId]['adds_to_cart'] += $item['adds_to_cart']; + $aggregatedUsers[$thirdUserId]['cost_per_atc'] += $item['cost_per_atc']; + $aggregatedUsers[$thirdUserId]['purchases'] += $item['purchases']; + $aggregatedUsers[$thirdUserId]['purchases_value'] += $item['purchases_value']; + $aggregatedUsers[$thirdUserId]['revenue'] += $item['revenue']; + $aggregatedUsers[$thirdUserId]['total_cost'] += $item['total_cost']; + $aggregatedUsers[$thirdUserId]['assisted_purchases'] += $item['assisted_purchases']; + $aggregatedUsers[$thirdUserId]['last_clicked_purchases'] += $item['last_clicked_purchases']; + } + +// 计算统计口径字段:ROAS, CTR等 + foreach ($aggregatedUsers as $userId => $data) { + $total_spend = $data['spend']; + $total_purchases_value = $data['purchases_value']; + $total_revenue = $data['revenue']; + $total_cost = $data['total_cost']; + $total_clicks = $data['clicks']; + $total_impressions = $data['impressions']; +// $total_adds_to_cart = $data['adds_to_cart']; +// $total_cost_per_atc = $data['cost_per_atc']; +// $total_purchases = $data['purchases']; +// $total_assisted_purchases = $data['assisted_purchases']; +// $total_last_clicked_purchases = $data['last_clicked_purchases']; + + // 计算 ROAS, CTR 和其他需要的字段 + $aggregatedUsers[$userId]['roas'] = $total_spend === 0 ? '-' : round($total_purchases_value / $total_spend, 2); + $aggregatedUsers[$userId]['amount_spend'] = '$' . number_format($total_spend, 2) ?: '$0.00'; + $aggregatedUsers[$userId]['purchases_value'] = '$' . number_format($total_purchases_value, 2) ?: '$0.00'; + $aggregatedUsers[$userId]['revenue'] = '$' . number_format($total_revenue, 2) ?: '$0.00'; + $aggregatedUsers[$userId]['total_cost'] = '$' . number_format($total_cost, 2) ?: '$0.00'; + $aggregatedUsers[$userId]['conversion_rate'] = $total_impressions === 0 ? '-' : round(($total_clicks / $total_impressions) * 100, 2) . '%'; + $aggregatedUsers[$userId]['ctr'] = $total_impressions === 0 ? '-' : round(($total_clicks / $total_impressions) * 100, 2) . '%'; + } + + // 最终分页信息 + $pagination = [ + 'startIndex' => ($page - 1) * $pageSize, + 'maxResults' => $pageSize, + 'count' => $users->total(), + 'pageNo' => $page, + 'pageSize' => $pageSize, + 'pages' => ceil($users->total() / $pageSize), + ]; + + return [ + 'pagination' => $pagination, + 'statistics' => $statistics, + 'data' => $aggregatedUsers, + ]; + } + + + /** + * 获取广告资产报告 + * + * @param string $keyword 关键词(广告素材名称模糊搜索) + * @param string $dateRange 日期范围:Today, Yesterday, Last Week, Last Month, Last Year + * @param string|null $startDate 起始日期(可选) + * @param string|null $endDate 结束日期(可选) + * @param int $page 当前页码 + * @param int $pageSize 每页数量 + * @return array + */ + public function getAssetConversionData($customerIds, $page, $pageSize, $keyword, $dateRange, $startDate = null, $endDate = null) + { + + $isSameMonth = true; // 判断是否跨月 + $currentMonth = date('Y-m'); //一个月内的统计需要确定是什么月份。 + if ($startDate && $endDate) { + // 比较日期的年月是否相同,若不同则为跨月 + $isSameMonth = date('Y-m', strtotime($startDate)) === date('Y-m', strtotime($endDate)); + if ($isSameMonth) { + $currentMonth = date('Y-m', strtotime($startDate)); + } + } + + // 1. 获取符合条件的 ad_id 列表,并且筛选 AssetRelation 的 date 字段 + $adIdsQuery = AssetRelation::alias('r') + ->leftJoin('bps.bps_google_ads_asset a', 'r.asset_id = a.asset_id') // 关联资产表 + ->where(function ($query) use ($keyword) { + if ($keyword) { + $query->where('a.asset_name', 'like', '%' . $keyword . '%'); // 关键词模糊匹配 + } + }); + + // 如果提供了 customerIds,增加查询条件 + if (!empty($customerIds)) { + $adIdsQuery->whereIn('a.customer_id', $customerIds); // 添加 customer_id 的查询约束 + } else { + return [ + 'data' => [], + 'chat_1_data' => [], // 返回按月汇总的 chat_data + 'total' => 0, + 'statistics' => [], + 'pagination' => [ + 'startIndex' => 0, + 'maxResults' => $pageSize, + 'count' => 0, + 'pageNo' => 1, + 'pageSize' => $pageSize, + 'pages' => 1 + ] + ]; + } + + // 日期范围处理,筛选 AssetRelation 的 date 字段 + if ($startDate && $endDate) { + $adIdsQuery->whereBetween('r.date', [$startDate, $endDate]); + } else { + switch ($dateRange) { + case 'Today': + $adIdsQuery->where('r.date', '=', date('Y-m-d')); + $currentMonth = date('Y-m'); + $isSameMonth = true; + break; + case 'Yesterday': + $adIdsQuery->where('r.date', '=', date('Y-m-d', strtotime('-1 day'))); + $currentMonth = date('Y-m', strtotime('-1 day')); + $isSameMonth = true; + break; + case 'Last Week': + $adIdsQuery->where('r.date', '>=', date('Y-m-d', strtotime('-1 week'))); + // 比较日期的年月是否相同,若不同则为跨月 + $isSameMonth = date('Y-m', strtotime('-1 week')) === date('Y-m'); + break; + case 'Last Month': + $adIdsQuery->where('r.date', '>=', date('Y-m-d', strtotime('-1 month'))); + $isSameMonth = false; + break; + case 'Last Year': + $adIdsQuery->where('r.date', '>=', date('Y-m-d', strtotime('-1 year'))); + $isSameMonth = false; + break; + default: + break; + } + } + + + // 获取唯一的 ad_id 列表 + $adIds = $adIdsQuery->distinct(true)->column('r.ad_id'); +// dump($adIds);return($adIds); + + // 2. 根据 ad_id 和日期范围去查询 DayData 表,按 ad_id 和日期聚合统计 + $dayDataQuery = DayData::alias('d') +// ->leftJoin('bps.bps_google_ads_campaign c', 'd.campaign_id = c.campaign_id') // 关联广告系列表(如果有) +// ->leftJoin('bps.bps_google_ads_ad_group ag', 'd.ad_group_id = ag.ad_group_id') // 关联广告组表(如果有) + ->whereIn('d.ad_id', $adIds) // 使用 ad_id 过滤 + ->where(function ($query) use ($startDate, $endDate, $dateRange) { + // 日期范围的筛选 + if ($startDate && $endDate) { + $query->whereBetween('d.date', [$startDate, $endDate]); + } else { + switch ($dateRange) { + case 'Today': + $query->where('d.date', '=', date('Y-m-d')); + break; + case 'Yesterday': + $query->where('d.date', '=', date('Y-m-d', strtotime('-1 day'))); + break; + case 'Last Week': + $query->where('d.date', '>=', date('Y-m-d', strtotime('-1 week'))); + break; + case 'Last Month': + $query->where('d.date', '>=', date('Y-m-d', strtotime('-1 month'))); + break; + case 'Last Year': + $query->where('d.date', '>=', date('Y-m-d', strtotime('-1 year'))); + break; + default: + break; + } + } + }); + + // 如果跨月,按月聚合;如果不跨月,按 ad_id 聚合 + if (!$isSameMonth) { + $dayDataQuery->group("d.month,d.ad_id,d.ad_name, d.ad_resource_name, d.ad_group_id, d.campaign_id, d.customer_id"); // 按月聚合 + $dayDataQuery->field([ + 'd.ad_id', + 'd.month', +// ThinkDb::raw("TO_CHAR(d.date, 'YYYY-MM') AS month"), // 格式化为 YYYY-MM 格式 + ThinkDb::raw('SUM(d.cost_micros) / 1000000 AS total_spend'), + ThinkDb::raw('SUM(d.conversions_value) AS total_conversions_value'), + ThinkDb::raw('SUM(d.conversions) AS total_conversions'), + ThinkDb::raw('SUM(d.impressions) AS total_impressions'), + ThinkDb::raw('SUM(d.clicks) AS total_clicks') + ]); + } else { + $dayDataQuery->group('d.ad_id,d.ad_name, d.ad_resource_name, d.ad_group_id, d.campaign_id, d.customer_id'); // 按 ad_id 聚合 + $dayDataQuery->field([ + 'd.ad_id', + ThinkDb::raw('SUM(d.cost_micros) / 1000000 AS total_spend'), + ThinkDb::raw('SUM(d.conversions_value) AS total_conversions_value'), + ThinkDb::raw('SUM(d.conversions) AS total_conversions'), + ThinkDb::raw('SUM(d.impressions) AS total_impressions'), + ThinkDb::raw('SUM(d.clicks) AS total_clicks') + ]); + } + + // 获取聚合数据,不需要再传递字段给 select() + $aggregatedData = $dayDataQuery->select(); +// dump($aggregatedData);return($aggregatedData); + + // 3. 获取与 ad_id 关联的 asset_id 以及相关数据 + $assetRelationsQuery = AssetRelation::whereIn('ad_id', $adIds) + ->whereBetween('date', [$startDate, $endDate]) // 加上 AssetRelation 的 date 过滤 + ->field(['asset_id', 'ad_id'])->select(); +// dump($assetRelationsQuery);return($assetRelationsQuery); +// 4. 汇总每个 asset_id 下的所有 ad_id 的聚合数据 + $assetSummaryData = []; + + foreach ($assetRelationsQuery as $assetRelation) { + // 从聚合数据中找到当前 ad_id 的相关数据 +// $adStats = $aggregatedData->where('ad_id', $assetRelation->ad_id)->first(); + // 获取该 ad_id 的所有聚合数据(可能有多条记录) + $adStatsCollection = $aggregatedData->where('ad_id', $assetRelation->ad_id); +// dump($adStatsCollection);return($adStatsCollection); + if (!$adStatsCollection->isEmpty()) { + // 如果该 ad_id 有对应的聚合数据,初始化资产汇总数据 + if (!isset($assetSummaryData[$assetRelation->asset_id])) { + + + $assetSummaryData[$assetRelation->asset_id] = [ + 'asset_id' => $assetRelation->asset_id, + 'asset_name' => '-', // 获取 asset_name + 'asset_type' => 0, // 获取 asset_type + 'asset_url' => '-', // 获取 asset_url + 'total_spend' => 0, + 'total_conversions_value' => 0, + 'total_conversions' => 0, + 'total_impressions' => 0, + 'ad_count' => 0, + 'monthly_data' => [], // 按月存储数据 + ]; + } + + // 遍历该 ad_id 所有的统计数据 + foreach ($adStatsCollection as $adStats) { + // 获取当前月份 + if ($isSameMonth) { + $month = $currentMonth; // 格式化为 YYYY-MM + } else { + $month = $adStats->month; // 格式化日期为 'YYYY-MM' + } + // 获取 asset 相关信息,通过模型查询 Asset + $asset = Asset::find($assetRelation->asset_id); // 根据 asset_id 查找对应的 Asset 数据 + // 累加该 ad_id 的统计数据到对应的 asset_id 汇总 + $assetSummaryData[$assetRelation->asset_id]['asset_name'] = $asset->asset_name; + $assetSummaryData[$assetRelation->asset_id]['asset_type'] = $asset->asset_type; + $assetSummaryData[$assetRelation->asset_id]['asset_url'] = $asset->asset_url; + $assetSummaryData[$assetRelation->asset_id]['total_spend'] += $adStats->total_spend; + $assetSummaryData[$assetRelation->asset_id]['total_conversions_value'] += $adStats->total_conversions_value; + $assetSummaryData[$assetRelation->asset_id]['total_conversions'] += $adStats->total_conversions; + $assetSummaryData[$assetRelation->asset_id]['total_impressions'] += $adStats->total_impressions; + $assetSummaryData[$assetRelation->asset_id]['ad'][] = $adStats->ad_id; // 存储 ad_id下一步统计数量 + + // 按月分开存储每个月的 spend 和 ROAS + if (!isset($assetSummaryData[$assetRelation->asset_id]['monthly_data'][$month])) { + $assetSummaryData[$assetRelation->asset_id]['monthly_data'][$month] = [ + 'month' => $month, + 'spend' => 0, + 'conversions_value' => 0, + ]; + } + + // 累加每个月的数据 + $assetSummaryData[$assetRelation->asset_id]['monthly_data'][$month]['spend'] += $adStats->total_spend; + $assetSummaryData[$assetRelation->asset_id]['monthly_data'][$month]['conversions_value'] += $adStats->total_conversions_value; // 避免除以零 + } + } + } +//return $assetSummaryData; + // 5. 格式化输出数据 + // 生成最终输出的数据 + $resultData = []; + $chat_data = []; + $statistics = $this->initializeStatistics(); // Initialize statistics before processing + + foreach ($assetSummaryData as $assetId => $data) { + // 计算 ROAS + $roas = $data['total_spend'] ? ($data['total_conversions_value'] / $data['total_spend']) : 0; + + // 合并月度数据到总数据 + $resultData[] = [ + 'creative_id' => $assetId, + 'creative' => $data['asset_name'], + 'creative_type' => $data['asset_type'], + 'creative_url' => $data['asset_url'], + 'spend' => '$' . number_format($data['total_spend'], 2), + 'purchase_value' => '-', + 'roas' => number_format($roas, 2) . 'X', + 'cpa' => '-', + 'cpc_link_click' => '-', + 'cpm' => '-', + 'cpc_all' => '-', + 'aov' => '-', + 'click_to_atc_ratio' => '-', + 'atc_to_purchase_ratio' => '-', + 'purchases' => '-', + 'first_frame_retention' => '-', + 'thumbstop' => '-', + 'ctr_outbound' => '-', + 'click_to_purchase' => '-', + 'ctr_all' => '-', + 'video_plays_25_rate' => '-', + 'video_plays_50_rate' => '-', + 'video_plays_75_rate' => '-', + 'video_plays_100_rate' => '-', + 'hold_rate' => '-', + 'total_conversions_value' => '$' . number_format($data['total_conversions_value'], 2), + 'total_conversions' => $data['total_conversions'], + 'total_impressions' => $data['total_impressions'], + 'ad_count' => count($data['ad']), + +// 'monthly_data' => $data['monthly_data'], // 合并月度数据 + ]; + + // 计算每个月的汇总数据,用于 chat_data + foreach ($data['monthly_data'] as $month => $monthlyData) { + // 如果 chat_data 中已有该月的数据,则累加 + if (!isset($chat_data[$month])) { + $chat_data[$month] = [ + 'month' => $month, + 'total_spend' => 0, + 'total_conversions_value' => 0, + 'roas' => 0, + ]; + } + + // 累加该月的 spend 和 conversions_value + $chat_data[$month]['total_spend'] += $monthlyData['spend']; + $chat_data[$month]['total_conversions_value'] += $monthlyData['conversions_value']; + + // Aggregate statistics for overall summary + $statistics['spend'] += $monthlyData['spend']; + $statistics['conversions_value'] += $monthlyData['conversions_value']; + + } + + + } + + // 计算每个月的总 ROAS,ROAS = 总的转换值 / 总的支出 + foreach ($chat_data as $month => $data) { + // 计算 ROAS + $totalSpend = $data['total_spend']; + $totalConversionsValue = $data['total_conversions_value']; + + // 如果有支出数据,计算 ROAS + $chat_data[$month]['roas'] = $totalSpend ? ($totalConversionsValue / $totalSpend) : 0; + // 格式化 ROAS 为倍数 + $chat_data[$month]['roas'] = round($chat_data[$month]['roas'], 2); + } + + // 计算整体的 ROAS + $statistics['spend'] = '$' . number_format($statistics['spend'], 2); + $statistics['roas'] = $statistics['spend'] > 0 ? ($statistics['conversions_value'] / $statistics['spend']) : 0; + + + // 返回分页数据 + $totalItems = count($assetSummaryData); + $totalPages = ceil($totalItems / $pageSize); + $startIndex = ($page - 1) * $pageSize; + // 截取返回的数据 +// $pagedData = array_slice($assetSummaryData, $startIndex, $pageSize); + $resultDataPaginated = array_slice($resultData, ($page - 1) * $pageSize, $pageSize); + + return [ + 'pagination' => [ + 'startIndex' => $startIndex, + 'maxResults' => $pageSize, + 'count' => $totalItems, + 'pageNo' => $page, + 'pageSize' => $pageSize, + 'pages' => $totalPages + ], + 'chat_1_data' => array_values($chat_data), // 返回按月汇总的 chat_data + 'data' => $resultDataPaginated, + 'statistics' => $statistics + ]; + } + + /** + * 初始化统计数据 + */ + private function initializeStatistics() + { + return [ + 'conversions_value' => 0, + 'spend' => 0, + 'purchase_value' => '-', // 可根据需求进一步计算 + 'roas' => 0, // 可以根据需要计算总体 ROAS + 'cpa' => '-', + 'cpc_link_click' => '-', + 'cpm' => '-', + 'cpc_all' => '-', + 'aov' => '-', + 'click_to_atc_ratio' => '-', + 'atc_to_purchase_ratio' => '-', + 'purchases' => '-', + 'first_frame_retention' => '-', + 'thumbstop' => '-', + 'ctr_outbound' => '-', + 'click_to_purchase' => '-', + 'ctr_all' => '-', + 'video_plays_25_rate' => '-', + 'video_plays_50_rate' => '-', + 'video_plays_75_rate' => '-', + 'video_plays_100_rate' => '-', + 'hold_rate' => '-' + // Add other stats as necessary + ]; + } +} diff --git a/app/service/BpsAdAccountService.php b/app/service/BpsAdAccountService.php new file mode 100644 index 0000000..a6478fa --- /dev/null +++ b/app/service/BpsAdAccountService.php @@ -0,0 +1,271 @@ + $third_user_id]; + + return ThinkDb::query($sql, $data); + } + + /** + * 批量获取全部Google广告账号数据 + * + */ + public function getGoogleAdAccounts($options = []) + { + if (!empty($options)) { + $refreshToken = $options['refresh_token']; + // 获取符合条件的客户ID数组 + $customers = ThirdUserAdvertiser::alias('tua') + ->join('bps.bps_third_user tu', 'tua.doc_ = tu.id') // 连接 bps_third_user 表 + ->where('tu.third_type', 'google') // 筛选 third_type 为 google 的记录 + ->where('tu.access_token', $refreshToken) // 筛选 third_type 为 google 的记录 + ->field('CAST(tua.advertiser_id AS BIGINT) as account_id,tua.google_login_customer_id as login_customer_id,tua.google_test_account as test_account,tua.google_manager as manager, tu.access_token as refresh_token') // 获取 advertiser_id 字段 + ->select(); // 执行查询 + } else { + // 获取符合条件的客户ID数组 + $customers = ThirdUserAdvertiser::alias('tua') + ->join('bps.bps_third_user tu', 'tua.doc_ = tu.id') // 连接 bps_third_user 表 + ->where('tu.third_type', 'google') // 筛选 third_type 为 google 的记录 + ->field('CAST(tua.advertiser_id AS BIGINT) as account_id,tua.google_login_customer_id as login_customer_id,tua.google_test_account as test_account,tua.google_manager as manager, tu.access_token as refresh_token') // 获取 advertiser_id 字段 + ->select(); // 执行查询 + } + + + // 如果没有找到符合条件的广告主,抛出异常 + if ($customers->isEmpty()) { + return []; +// throw new ApiException('No customers found for google third type'); + } + + // 转换为简单的数组(提取 advertiser_id) + return $customers->toArray(); + + } + + /** + * 批量获取全部meta广告账号数据 + * + */ + public function getMetaAdAccounts($options = []) + { + if (!empty($options)) { + $refreshToken = $options['refresh_token']; + // 获取符合条件的客户ID数组 + $customers = ThirdUserAdvertiser::alias('tua') + ->join('bps.bps_third_user tu', 'tua.doc_ = tu.id') // 连接 bps_third_user 表 + ->where('tu.third_type', 'facebook') // 筛选 third_type 为 google 的记录 + ->where('tu.access_token', $refreshToken) // 筛选 third_type 为 google 的记录 + ->field('CAST(tua.advertiser_id AS BIGINT) as account_id, tu.access_token as refresh_token') // 获取 advertiser_id 字段 + ->select(); // 执行查询 + } else { + // 获取符合条件的客户ID数组 + $customers = ThirdUserAdvertiser::alias('tua') + ->join('bps.bps_third_user tu', 'tua.doc_ = tu.id') // 连接 bps_third_user 表 + ->where('tu.third_type', 'facebook') // 筛选 third_type 为 google 的记录 + ->field('CAST(tua.advertiser_id AS BIGINT) as account_id, tu.access_token as refresh_token') // 获取 advertiser_id 字段 + ->select(); // 执行查询 + } + + + // 如果没有找到符合条件的广告主,抛出异常 + if ($customers->isEmpty()) { + return []; +// throw new ApiException('No customers found for google third type'); + } + + // 转换为简单的数组(提取 advertiser_id) + return $customers->toArray(); + + } + + /** + * 批量获取全部meta广告账号数据 + * + */ + public function getTiktokAdAccounts($options = []) + { + if (!empty($options)) { + $refreshToken = $options['refresh_token']; + // 获取符合条件的客户ID数组 + $customers = ThirdUserAdvertiser::alias('tua') + ->join('bps.bps_third_user tu', 'tua.doc_ = tu.id') // 连接 bps_third_user 表 + ->where('tu.third_type', 'tiktok') // 筛选 third_type 为 google 的记录 + ->where('tu.access_token', $refreshToken) // 筛选 third_type 为 google 的记录 + ->field('CAST(tua.advertiser_id AS BIGINT) as account_id, tu.access_token as refresh_token') // 获取 advertiser_id 字段 + ->select(); // 执行查询 + } else { + // 获取符合条件的客户ID数组 + $customers = ThirdUserAdvertiser::alias('tua') + ->join('bps.bps_third_user tu', 'tua.doc_ = tu.id') // 连接 bps_third_user 表 + ->where('tu.third_type', 'tiktok') // 筛选 third_type 为 google 的记录 + ->field('CAST(tua.advertiser_id AS BIGINT) as account_id, tu.access_token as refresh_token') // 获取 advertiser_id 字段 + ->select(); // 执行查询 + } + + + // 如果没有找到符合条件的广告主,抛出异常 + if ($customers->isEmpty()) { + return []; +// throw new ApiException('No customers found for google third type'); + } + + // 转换为简单的数组(提取 advertiser_id) + return $customers->toArray(); + + } + + /** + * 批量获取全部meta广告账号数据 + * + */ + public function getAllAdAccounts($options = []) + { + // 获取符合条件的客户ID数组 + $customers = ThirdUserAdvertiser::alias('tua') + ->join('bps.bps_third_user tu', 'tua.doc_ = tu.id') // 连接 bps_third_user 表 + ->where('tu.user_id', $options['uid']) // 筛选 third_type 为 google 的记录 + ->field('CAST(tua.advertiser_id AS BIGINT) as account_id, tu.access_token as refresh_token') // 获取 advertiser_id 字段 + ->select(); // 执行查询 + + + // 如果没有找到符合条件的广告主,抛出异常 + if ($customers->isEmpty()) { + return []; +// throw new ApiException('No customers found for google third type'); + } + + // 转换为简单的数组(提取 advertiser_id) + return $customers->toArray(); + + } + + // 获取 Facebook 第三方用户数据 + public function getMetaThirdUsers($options = []) + { + $uid = $options['uid'] ?? null; + + if ($uid) { + // 根据 UID 获取符合条件的用户数据 + $users = ThirdUserAdvertiser::alias('tua') + ->join('bps.bps_third_user tu', 'tua.doc_ = tu.id') // 连接 bps_third_user 表 + ->where('tu.third_type', 'facebook') // 筛选 third_type 为 facebook 的记录 + ->where('tu.user_id', $uid) // 使用 UID 筛选用户 + ->field('tu.id, tu.access_token as refresh_token') // 获取相关字段 + ->select(); + } else { + // 获取符合条件的用户数据(如果没有 UID) + $users = ThirdUserAdvertiser::alias('tua') + ->join('bps.bps_third_user tu', 'tua.doc_ = tu.id') // 连接 bps.bps_third_user 表 + ->where('tu.third_type', 'facebook') // 筛选 third_type 为 facebook 的记录 + ->field('tu.id, tu.access_token as refresh_token') // 获取相关字段 + ->select(); + } + + // 如果没有找到符合条件的用户,返回空数组 + if ($users->isEmpty()) { + return []; + } + + return $users->toArray(); + } + + +// 获取 Google 第三方用户数据 + public function getGoogleThirdUsers($options = []) + { + if (!empty($options)) { + $refreshToken = $options['refresh_token']; + // 获取符合条件的用户数据 + $users = ThirdUserAdvertiser::alias('tua') + ->join('bps.bps_third_user tu', 'tua.doc_ = tu.id') // 连接 bps_third_user 表 + ->where('tu.third_type', 'google') // 筛选 third_type 为 google 的记录 + ->where('tu.access_token', $refreshToken) // 筛选 third_type 为 google 的记录 + ->field('tu.id, tu.access_token as refresh_token') // 获取相关字段 + ->select(); + } else { + // 获取符合条件的用户数据 + $users = ThirdUserAdvertiser::alias('tua') + ->join('bps.bps_third_user tu', 'tua.doc_ = tu.id') // 连接 bps_third_user 表 + ->where('tu.third_type', 'google') // 筛选 third_type 为 google 的记录 + ->field('tu.id, tu.access_token as refresh_token') // 获取相关字段 + ->select(); + } + + // 如果没有找到符合条件的用户,返回空数组 + if ($users->isEmpty()) { + return []; + } + + return $users->toArray(); + } + +// 获取 TikTok 第三方用户数据 + public function getTiktokThirdUsers($options = []) + { + if (!empty($options)) { + $refreshToken = $options['refresh_token']; + // 获取符合条件的用户数据 + $users = ThirdUserAdvertiser::alias('tua') + ->join('bps.bps_third_user tu', 'tua.doc_ = tu.id') // 连接 bps_third_user 表 + ->where('tu.third_type', 'tiktok') // 筛选 third_type 为 tiktok 的记录 + ->where('tu.access_token', $refreshToken) // 筛选 third_type 为 tiktok 的记录 + ->field('tu.id, tu.access_token as refresh_token') // 获取相关字段 + ->select(); + } else { + // 获取符合条件的用户数据 + $users = ThirdUserAdvertiser::alias('tua') + ->join('bps.bps_third_user tu', 'tua.doc_ = tu.id') // 连接 bps_third_user 表 + ->where('tu.third_type', 'tiktok') // 筛选 third_type 为 tiktok 的记录 + ->field('tu.id, tu.access_token as refresh_token') // 获取相关字段 + ->select(); + } + + // 如果没有找到符合条件的用户,返回空数组 + if ($users->isEmpty()) { + return []; + } + + return $users->toArray(); + } + +// 获取所有平台的第三方用户数据 + public function getAllThirdUsers($options = []) + { + $userId = $options['uid']; // 获取用户ID + + // 获取符合条件的用户数据 + $users = ThirdUserAdvertiser::alias('tua') + ->join('bps.bps_third_user tu', 'tua.doc_ = tu.id') // 连接 bps_third_user 表 + ->where('tu.user_id', $userId) // 筛选 user_id 的记录 + ->field('tu.id, tu.access_token as refresh_token') // 获取相关字段 + ->select(); + + // 如果没有找到符合条件的用户,返回空数组 + if ($users->isEmpty()) { + return []; + } +//dump($users->toArray()); + return $users->toArray(); + } + + +} \ No newline at end of file diff --git a/app/service/GoogleOAuthService.php b/app/service/GoogleOAuthService.php index e2a5cbc..54f2f7e 100644 --- a/app/service/GoogleOAuthService.php +++ b/app/service/GoogleOAuthService.php @@ -278,7 +278,7 @@ class GoogleOAuthService */ public function getGoogleAdCustomers($options = []) { - if (!empty($options)) { + if (!empty($options['refresh_token'])) { $refreshToken = $options['refresh_token']; // 获取符合条件的客户ID数组 $customers = ThirdUserAdvertiser::alias('tua') diff --git a/config/route.php b/config/route.php index d12d88f..656e59a 100644 --- a/config/route.php +++ b/config/route.php @@ -14,6 +14,7 @@ //use app\controller\IndexController; use app\controller\AdController; +use app\controller\BpsAdController; use app\controller\OAuthController; use app\controller\CustomerController; use app\controller\GoogleAdsController; @@ -30,6 +31,60 @@ use Webman\Route; // }); //}); +//三平台合并后的路由2025-1-9 +Route::group('/marketing', function () { + Route::group('/v18', function () { + Route::group('/ad', function () { + Route::post('/list', [BpsAdController::class, 'listAds']); +// Route::post('/export', [BpsAdController::class, 'exportAdsToExcel']); +// Route::group('/status', function () { +// Route::post('/update', [BpsAdController::class, 'updateAdStatus']); +//// Route::post('/get', [AdController::class, 'getAdStatus']); +// }); + })->middleware([ + app\middleware\JwtLocal::class, + app\middleware\OauthCheck::class, + ]); + + Route::group('/campaign', function () { + Route::post('/list', [BpsAdController::class, 'listCampaigns']); +// Route::post('/export', [BpsAdController::class, 'exportCampaignsToExcel']); +// Route::group('/status', function () { +// Route::post('/update', [BpsAdController::class, 'updateCampaignStatus']); +// Route::post('/get', [BpsAdController::class, 'getCampaignStatus']); +// }); + })->middleware([ + app\middleware\JwtLocal::class, + app\middleware\OauthCheck::class, + ]); + Route::group('/adset', function () { + Route::post('/list', [BpsAdController::class, 'listAdsets']); +// Route::post('/export', [BpsAdController::class, 'exportGroupsToExcel']); +// Route::group('/status', function () { +// Route::post('/update', [BpsAdController::class, 'updateGroupStatus']); +//// Route::post('/get', [BpsAdController::class, 'getGroupStatus']); +// }); + })->middleware([ + app\middleware\JwtLocal::class, + app\middleware\OauthCheck::class, + ]); + Route::group('/third_user', function () { + Route::post('/list', [BpsAdController::class, 'listThirdUsers']); + })->middleware([ + app\middleware\JwtLocal::class, + app\middleware\OauthCheck::class, + ]); + Route::group('/asset', function () { + Route::post('/list', [BpsAdController::class, 'listAssets']); + })->middleware([ + app\middleware\JwtLocal::class, + app\middleware\OauthCheck::class, + ]); + + }); +}); + +//未合并前使用的google广告路由 Route::group('/googleads', function () { Route::group('/v18', function () { @@ -41,7 +96,7 @@ Route::group('/googleads', function () { // Route::post('/get', [AdController::class, 'getAdStatus']); }); })->middleware([ - app\middleware\Jwt::class, + app\middleware\JwtLocal::class, app\middleware\OauthCheck::class, ]); @@ -53,7 +108,7 @@ Route::group('/googleads', function () { // Route::post('/get', [AdController::class, 'getCampaignStatus']); }); })->middleware([ - app\middleware\Jwt::class, + app\middleware\JwtLocal::class, app\middleware\OauthCheck::class, ]); Route::group('/adgroup', function () { @@ -64,13 +119,13 @@ Route::group('/googleads', function () { // Route::post('/get', [AdController::class, 'getGroupStatus']); }); })->middleware([ - app\middleware\Jwt::class, + app\middleware\JwtLocal::class, app\middleware\OauthCheck::class, ]); Route::group('/asset', function () { Route::post('/list', [AdController::class, 'listAssets']); })->middleware([ - app\middleware\Jwt::class, + app\middleware\JwtLocal::class, app\middleware\OauthCheck::class, ]); Route::group('/customer', function () { @@ -78,8 +133,9 @@ Route::group('/googleads', function () { Route::post('/bind', [CustomerController::class, 'bindAdvertiser']); Route::post('/list_resource', [CustomerController::class, 'accessibleCustomers']); Route::post('/list_tree', [CustomerController::class, 'accountHierarchy']); + Route::post('/access_role', [CustomerController::class, 'accountAccess']); })->middleware([ - app\middleware\Jwt::class, + app\middleware\JwtLocal::class, app\middleware\OauthCheck::class, ]); @@ -91,7 +147,7 @@ Route::group('/googleads', function () { Route::post('/refresh_token_test', [OAuthController::class, 'testRefreshToken']); Route::post('/refresh_token_revoke', [OAuthController::class, 'revokeRefreshToken']); })->middleware([ - app\middleware\Jwt::class, +// app\middleware\JwtLocal::class, ]); });