From d1713a5189a314edb485dc090c92ed0e4a3188f8 Mon Sep 17 00:00:00 2001 From: hgc Date: Thu, 26 Dec 2024 21:13:15 +0800 Subject: [PATCH] =?UTF-8?q?=E8=B0=B7=E6=AD=8C=E5=88=9B=E6=84=8F=E7=B4=A0?= =?UTF-8?q?=E6=9D=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/command/SyncGoogleAdsAsset.php | 59 ++ app/controller/AssetController.php | 18 + app/controller/GoogleAdsController.php | 26 + app/controller/OAuthController.php | 7 +- app/event/GoogleAdsAssets.php | 282 ++++++++++ app/model/Ad.php | 101 +++- app/model/AdGroup.php | 11 +- app/model/Asset.php | 56 ++ app/model/AssetRelation.php | 43 ++ app/model/Campaign.php | 5 + app/process/UpdateGoogleAdsTask.php | 8 + app/schedule.php | 5 + app/service/GoogleAdsAdService.php | 142 +++-- app/service/GoogleAdsAssetService.php | 741 +++++++++++++++++++++++++ config/event.php | 4 + config/route.php | 3 + 16 files changed, 1471 insertions(+), 40 deletions(-) create mode 100644 app/command/SyncGoogleAdsAsset.php create mode 100644 app/controller/AssetController.php create mode 100644 app/event/GoogleAdsAssets.php create mode 100644 app/model/Asset.php create mode 100644 app/model/AssetRelation.php create mode 100644 app/schedule.php create mode 100644 app/service/GoogleAdsAssetService.php diff --git a/app/command/SyncGoogleAdsAsset.php b/app/command/SyncGoogleAdsAsset.php new file mode 100644 index 0000000..a941dd3 --- /dev/null +++ b/app/command/SyncGoogleAdsAsset.php @@ -0,0 +1,59 @@ +setName('sync:google_ads_asset') + ->setDescription('Synchronize Google Ads Asset relations'); + } + + protected function execute(Input $input, Output $output) + { + // 获取所有素材 + $assets = GoogleAdsAsset::where('status', 1)->select(); + + foreach ($assets as $asset) { + // 获取广告表中的所有广告 + $ads = GoogleAdsAd::where('status', 1)->select(); + + foreach ($ads as $ad) { + // 检查广告的 metadata 是否包含素材的 resource_name + if (isset($ad->metadata) && strpos($ad->metadata, $asset->resource_name) !== false) { + // 插入关联记录 + $existingRelation = GoogleAdsAssetRelations::where('asset_id', $asset->asset_id) + ->where('ad_id', $ad->ad_id) + ->where('date', date('Y-m-d')) + ->find(); + + if (!$existingRelation) { + // 如果没有现有记录,则插入新的关联 + GoogleAdsAssetRelations::create([ + 'asset_id' => $asset->asset_id, + 'ad_id' => $ad->ad_id, + 'ad_group_id' => $ad->ad_group_id, + 'campaign_id' => $ad->campaign_id, + 'date' => date('Y-m-d') + ]); + + $output->writeln("Inserted relation for Asset ID: {$asset->asset_id} and Ad ID: {$ad->ad_id}"); + } else { + $output->writeln("Relation already exists for Asset ID: {$asset->asset_id} and Ad ID: {$ad->ad_id}"); + } + } + } + } + + $output->writeln('Google Ads Asset synchronization completed.'); + } +} diff --git a/app/controller/AssetController.php b/app/controller/AssetController.php new file mode 100644 index 0000000..4c2f8da --- /dev/null +++ b/app/controller/AssetController.php @@ -0,0 +1,18 @@ + 'Synchronization completed successfully']); + } +} diff --git a/app/controller/GoogleAdsController.php b/app/controller/GoogleAdsController.php index 2813b45..c4e79e7 100644 --- a/app/controller/GoogleAdsController.php +++ b/app/controller/GoogleAdsController.php @@ -9,6 +9,7 @@ use support\Request; use app\service\GoogleAdsCampaignService; use app\service\GoogleAdsGroupService; use app\service\GoogleAdsAdService; +use app\service\GoogleAdsAssetService; use app\service\GoogleAdsAccountService; use support\Response; use DI\Annotation\Inject; @@ -35,6 +36,12 @@ class GoogleAdsController */ private $googleAdsAdService; + /** + * @Inject + * @var GoogleAdsAssetService + */ + private $googleAdsAssetService; + /** * @Inject * @var GoogleAdsAccountService @@ -79,6 +86,14 @@ class GoogleAdsController return $this->getAds($options); } + public function listAssets(Request $request) + { + $options = $request->all(); + + // 继续处理 Google Ads API 操作 + return $this->getAssets($options); + } + public function listDateDatas(Request $request) { $options = $request->all(); @@ -254,6 +269,17 @@ class GoogleAdsController return $this->successResponse(['groups_list' => $resourceName]); } + /** + * get assets + * @throws ApiException + */ + public function getAssets($options): Response + { + $resourceName = $this->googleAdsAssetService->runListAssets($options['customer_id']); +// return $this->successResponse(['assets_list' => $resourceName]); + return $this->successResponse(['assets_list' => 'succeed added']); + } + /** * get date datas * @throws ApiException diff --git a/app/controller/OAuthController.php b/app/controller/OAuthController.php index da6e426..f3a1a23 100644 --- a/app/controller/OAuthController.php +++ b/app/controller/OAuthController.php @@ -11,6 +11,7 @@ use app\event\GoogleAdsCampaigns; use app\event\GoogleAdsGroups; use app\event\GoogleAdsAds; use app\event\GoogleAdsDateDatas; +use app\event\GoogleAdsMaterials; use Webman\Event\Event; class OAuthController @@ -120,8 +121,10 @@ class OAuthController // Event::emit(GoogleAdsCampaigns::type, []); // dump(date('Y-m-d H:i:s') . '更新' . GoogleAdsGroups::type . '开始'); // Event::emit(GoogleAdsGroups::type, []); - dump(date('Y-m-d H:i:s') . '更新' . GoogleAdsAds::type . '开始'); - Event::emit(GoogleAdsAds::type, []); +// dump(date('Y-m-d H:i:s') . '更新' . GoogleAdsAds::type . '开始'); +// Event::emit(GoogleAdsAdMaterials::type, []); + dump(date('Y-m-d H:i:s') . '更新' . GoogleAdsMaterials::type . '开始'); + Event::emit(GoogleAdsMaterials::type, []); return $this->successResponse(['data' => []]); diff --git a/app/event/GoogleAdsAssets.php b/app/event/GoogleAdsAssets.php new file mode 100644 index 0000000..797ac5b --- /dev/null +++ b/app/event/GoogleAdsAssets.php @@ -0,0 +1,282 @@ +all(); + + // 继续处理 Google Ads API 操作 + return $this->getAssets($options); + } + + /** + * get assets + * @throws ApiException + */ + public function getAssets($options) + { + $customers = $this->googleOAuthService->getGoogleAdCustomers([]); + foreach ($customers as $customerId) { + $googleAdsAssetService = new GoogleAdsAssetService($customerId); + $resourceName = $googleAdsAssetService->runListAssets($customerId); + } + +// return $this->successResponse(['ads_list' => $resourceName]); + } + + /** + * 每天爬取tiktok广告 + * @return void + */ + public function update() + { + try { + + $client = new Client([ + //允许重定向 +// 'allow_redirects' => true, + ]); + + + // 获取前两天 0 点的时间戳 + $dayBeforeYesterdayStart = strtotime('-2 days 00:00:00'); + + // 获取前一天 0 点的时间戳 + $yesterdayStart = strtotime('-1 day 00:00:00'); + $countryCache = Redis::get(self::type . 'lastCountry'); + //全部跑完跳出 + if ($countryCache == 'All') { + dump($countryCache . '国家更新tiktok Ads 完成'); + return; + } + + if (empty($countryCache)) { + $countryCache = 'GB'; + } + + $searchIdCache = Redis::get(self::type . 'lastSearchId'); + $offsetCache = Redis::get(self::type . 'nextOffset'); + $totalCache = Redis::get(self::type . 'totalCache'); + if (!isset($searchIdCache) || empty($searchIdCache)) { + $searchIdCache = ''; + } + if (!isset($offsetCache) || empty($searchIdCache)) { + $offsetCache = 0; + } + if (!isset($totalCache) || empty($searchIdCache)) { + $totalCache = 0; + } + + //判断国家是否跑完。 + if ($totalCache > 0 && $offsetCache > ceil($totalCache / self::limit)) { + $key = array_search($countryCache, self::countries, true); + if (in_array($countryCache, self::countries) && isset(self::countries[$key + 1])) { + $countryCache = self::countries[$key + 1]; + dump('更新' . $countryCache . '国家的tiktok Ads 完成'); + } else { + $countryCache = 'All'; //赋值非正常国家的code中断定时任务 + dump($countryCache . '国家更新tiktok Ads 完成'); + } + return; + } + + + $start_time = $dayBeforeYesterdayStart; + $end_time = $yesterdayStart; + //当前国家爬取 + $currentParams = null; + $currentParams = ['country' => $countryCache, 'search_id' => $searchIdCache, 'type' => 1, 'start_time' => $start_time, 'end_time' => $end_time]; + + + $url = 'https://library.tiktok.com/api/v1/search?region=' . $currentParams['country'] . '&type=' . $currentParams['type'] . '&start_time=' . $currentParams['start_time'] . '&end_time=' . $currentParams['end_time']; + + $res = json_decode($client->post($url, [ + 'headers' => [ + 'accept' => 'application/json, text/plain, */*', + 'accept-language' => 'zh-CN,zh;q=0.9', + 'content-type' => 'application/json', + 'cookie' => 'cookie: _ttp=2ov8Fc4C2CaNscHJd90O9fMhlpE; _ga=GA1.1.1025820618.1731926196; FPID=FPID2.2.Bcgkp%2Fk%2Bbn5w5YeSMR9wd9VpNHJwTUpkkaEqSdCEa0w%3D.1731926196; FPAU=1.2.944915349.1731926193; FPLC=mbVyryI5aG6IVpAvhs1JsgWjA7FVA6QsCJ7VbXhM7zWoXNp4rcD0IK7FNTTf%2FuOrqeOgqEhTd4NB3hY7q3aDVTGQa3WGHqxkGte4%2BBZxsrpaHFas9kb7DPRXM12T5Q%3D%3D; _ga_TEQXTT9FE4=GS1.1.1732097542.7.0.1732097542.0.0.857840528', + 'origin' => 'https://library.tiktok.com', + 'priority' => 'u=1, i', + 'referer' => 'https://library.tiktok.com/ads?region=AT&start_time=1731945600000&end_time=1732032000000&adv_name=&adv_biz_ids=&query_type=&sort_type=last_shown_date,desc', + 'sec-ch-ua' => '"Google Chrome";v="131", "Chromium";v="131", "Not_A Brand";v="24"', + 'sec-ch-ua-mobile' => '?0', + 'sec-ch-ua-platform' => '"Windows"', + 'sec-fetch-dest' => 'empty', + 'sec-fetch-mode' => 'cors', + 'sec-fetch-site' => 'same-origin', + 'user-agent' => self::userAgent, +// 'user-agent' => 'user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36', + ], + 'json' => [ + 'query' => '', + 'query_type' => '', + 'adv_biz_ids' => '', + 'order' => self::sort_order, + 'offset' => (int)$offsetCache, + 'search_id' => $currentParams['search_id'], + 'limit' => self::limit, + ], + + ])->getBody()->getContents(), true); +// dump($res);return; //调试点 + + if ($res['search_id'] != $searchIdCache) { + $searchIdCache = $res['search_id']; + dump('search_id更新 ' . $searchIdCache . ' 成功'); + } + + + if ($res['total'] == 0 || $res['code'] != 0) { + dump('更新tiktok Ads接口响应异常:' . json_encode($res, JSON_UNESCAPED_UNICODE)); + return; + } + $listAdsIds = []; + foreach ($res['data'] as $ad) { + if ($ad['audit_status'] == 2 || empty($ad['videos'])) { + continue; //审核不过或者没视频不采集 + } + + $imagesJson = is_array($ad['image_urls']) ? json_encode($ad['image_urls']) : json_encode([]); + $rejection_info = is_array($ad['rejection_info']) ? json_encode($ad['rejection_info']) : null; +// dump($rejection_info); + $insertData[$ad['id']] = [ + 'ad_id' => $ad['id'], + 'name' => $ad['name'], + 'audit_status' => $ad['audit_status'], + 'type' => $ad['type'], + 'first_shown_date' => $ad['first_shown_date'], + 'last_shown_date' => $ad['last_shown_date'], + 'image_urls' => $imagesJson, + 'estimated_audience' => $ad['estimated_audience'], + 'spent' => $ad['spent'], + 'impression' => $ad['impression'], + 'show_mode' => $ad['show_mode'], + 'rejection_info' => $rejection_info, + 'sor_audit_status' => $ad['sor_audit_status'], + 'country_code' => $countryCache, + ]; + if (isset($ad['videos']) && !empty($ad['videos'])) { + // 遍历 "videos" 数组 + foreach ($ad['videos'] as $video) { + $insertData[$ad['id']]['video_url'] = $video['video_url']; + $insertData[$ad['id']]['cover_img'] = $video['cover_img']; + } + } + $listAdsIds = array_column($insertData, 'ad_id'); + + } +// dump($insertData);return; + + + if (empty($insertData)) return; + + //开启事务 + Db::beginTransaction(); + + + //删除原来的旧数据 + TiktokAd::query()->whereIn('ad_id', array_keys($insertData))->delete(); + //添加新的数据 + TiktokAd::query()->insert($insertData); + + //redis缓存 + Redis::set(self::type, json_encode($insertData, JSON_UNESCAPED_UNICODE)); + + //redis缓存 记录更新时间 + $time = date('Y-m-d H:i:s'); + Redis::set(self::type . 'time', $time); + Redis::set(self::type . 'lastCountry', $countryCache); + Redis::set(self::type . 'nextOffset', ++$offsetCache); //记录下一次的offset + Redis::set(self::type . 'totalCache', $res['total']); + Redis::set(self::type . 'lastSearchId', $searchIdCache); + if (!empty($listAdsIds)) { + Redis::rPush(self::type . 'AdsIds', ...$listAdsIds); + } + + //提交事务 + Db::commit(); + //销毁$res + unset($res); + + dump(date('Y-m-d H:i:s') . '更新' . self::type . '成功'); + } catch (GuzzleException|\Exception $exception) { + //回滚事务 + Db::rollBack(); + dump('更' . self::type . '异常:' . $exception->getMessage()); + dump($exception); + } +// } catch (ClientExceptionInterface $e) { +// // 捕获 4xx 错误 +// dump( 'Client error: ' . $e->getMessage() . "\n"); +// } catch (ServerExceptionInterface $e) { +// // 捕获 5xx 错误 +// dump('Server error: ' . $e->getMessage() . "\n"); +// } catch (TransportExceptionInterface $e) { +// // 捕获网络传输错误 +// dump('Transport error: ' . $e->getMessage() . "\n") ; +// } catch (\Exception $e) { +// // 捕获所有其他错误 +// dump('General error: ' . $e->getMessage() . "\n") ; +// } + } + + + // 可以加入一些公共方法 + protected function successResponse($data): Response + { + 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/Ad.php b/app/model/Ad.php index bb3e38a..f41910f 100644 --- a/app/model/Ad.php +++ b/app/model/Ad.php @@ -30,7 +30,7 @@ class Ad extends Model 'status' => 1, // 广告状态默认值为 'ENABLED' ]; - // 状态判断常量 + // 状态判断常量 const STATUS_UNSPECIFIED = 0; const STATUS_UNKNOWN = 1; const STATUS_ENABLED = 2; @@ -40,7 +40,7 @@ class Ad extends Model // 获取广告状态 public function getStatusTextAttr($value, $data) { - $statusMap = [ + $statusMap = [ self::STATUS_UNSPECIFIED => 'UNSPECIFIED', self::STATUS_UNKNOWN => 'UNKNOWN', self::STATUS_ENABLED => 'ENABLED', @@ -50,6 +50,97 @@ class Ad extends Model 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) // { @@ -84,6 +175,12 @@ class Ad extends Model return $this->belongsTo(AdGroup::class, 'ad_group_id', 'ad_group_id'); } + // 关联到素材关系 + public function assetRelations() + { + return $this->hasMany(AssetRelation::class, 'ad_id', 'ad_id'); + } + // 关联 Customer 模型(广告属于客户) // public function customer() // { diff --git a/app/model/AdGroup.php b/app/model/AdGroup.php index ad90d6c..380de5f 100644 --- a/app/model/AdGroup.php +++ b/app/model/AdGroup.php @@ -32,16 +32,17 @@ class AdGroup extends Model 'cpc_bid_micros' => 0, // 每次点击出价默认值为0 ]; - // 状态判断常量 + // 状态判断常量 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 = [ + $statusMap = [ self::STATUS_UNSPECIFIED => 'UNSPECIFIED', self::STATUS_UNKNOWN => 'UNKNOWN', self::STATUS_ENABLED => 'ENABLED', @@ -96,5 +97,11 @@ class AdGroup extends Model { return $this->hasMany(Ad::class, 'ad_group_id', 'ad_group_id'); } + + // 关联到素材关系 + public function assetRelations() + { + return $this->hasMany(AssetRelation::class, 'ad_group_id', 'ad_group_id'); + } } diff --git a/app/model/Asset.php b/app/model/Asset.php new file mode 100644 index 0000000..7833629 --- /dev/null +++ b/app/model/Asset.php @@ -0,0 +1,56 @@ + 'YOUTUBE_VIDEO', + self::TYPE_IMAGE => 'IMAGE' + ]; + return $statusMap[$data['asset_type']] ?? 'UNKNOWN'; + } + + // 允许批量赋值的字段 + protected $fillable = ['customer_id', 'asset_type', 'asset_name', 'resource_name','asset_url', 'status', 'metadata']; + + // 关联到广告、广告组和广告活动 + public function relations() + { + return $this->hasMany(AssetRelation::class, 'asset_id', 'asset_id'); + } + + // 获取素材的广告、广告组和广告活动 + public function getRelations() + { + return $this->relations()->with(['ad', 'adGroup', 'campaign']); + } + + // 追加自定义字段到模型结果中 + public function appendCustomAttributes() + { + return ['relations']; + } +} diff --git a/app/model/AssetRelation.php b/app/model/AssetRelation.php new file mode 100644 index 0000000..0cd5c86 --- /dev/null +++ b/app/model/AssetRelation.php @@ -0,0 +1,43 @@ +belongsTo(Ad::class, 'ad_id', 'ad_id'); + } + + // 关联到广告组 + public function adGroup() + { + return $this->belongsTo(AdGroup::class, 'ad_group_id', 'ad_group_id'); + } + + // 关联到广告活动 + public function campaign() + { + return $this->belongsTo(Campaign::class, 'campaign_id', 'campaign_id'); + } + + // 素材关联关系 + public function asset() + { + return $this->belongsTo(Asset::class, 'asset_id', 'asset_id'); + } +} diff --git a/app/model/Campaign.php b/app/model/Campaign.php index 2e2163e..72edd2f 100644 --- a/app/model/Campaign.php +++ b/app/model/Campaign.php @@ -105,4 +105,9 @@ class Campaign extends Model { 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/process/UpdateGoogleAdsTask.php b/app/process/UpdateGoogleAdsTask.php index 068fd1f..36ca56d 100644 --- a/app/process/UpdateGoogleAdsTask.php +++ b/app/process/UpdateGoogleAdsTask.php @@ -6,6 +6,7 @@ namespace app\process; use app\event\GoogleAdsCampaigns; use app\event\GoogleAdsGroups; use app\event\GoogleAdsAds; +use app\event\GoogleAdsAssets; use app\event\GoogleAdsDateDatas; use Webman\Event\Event; use Workerman\Crontab\Crontab; @@ -65,6 +66,13 @@ class UpdateGoogleAdsTask dump(date('Y-m-d H:i:s') . '更新' . GoogleAdsAds::type . '开始'); Event::emit(GoogleAdsAds::type, []); } + + ); + // 每15分钟执行一次 + new Crontab('58 */15 * * * *', function () { + dump(date('Y-m-d H:i:s') . '更新' . GoogleAdsAssets::type . '开始'); + Event::emit(GoogleAdsAssets::type, []); + } ); // 每15分钟执行一次 diff --git a/app/schedule.php b/app/schedule.php new file mode 100644 index 0000000..f0f67fc --- /dev/null +++ b/app/schedule.php @@ -0,0 +1,5 @@ +everyDay(); // 每天运行一次 diff --git a/app/service/GoogleAdsAdService.php b/app/service/GoogleAdsAdService.php index c2baebe..e0cde38 100644 --- a/app/service/GoogleAdsAdService.php +++ b/app/service/GoogleAdsAdService.php @@ -78,7 +78,7 @@ class GoogleAdsAdService extends BaseService // Creates a single shared budget to be used by the campaigns added below. $groupAdsResourceName = self::getAds($googleAdsClient->getGoogleAdsClient(), $customerId); -// dump(json_encode($groupadsResourceName)); +// dump(json_encode($groupAdsResourceName)); if (is_array($groupAdsResourceName)) { self::saveAds($groupAdsResourceName); } @@ -88,7 +88,7 @@ class GoogleAdsAdService extends BaseService /** - * 在数据库中保存广告系列信息 + * 在数据库中保存广告信息 * @param $groupadsResourceName * @return void */ @@ -97,24 +97,41 @@ class GoogleAdsAdService extends BaseService $tableName = 'bps_google_ads_ad'; $tableName = getenv('DB_PG_SCHEMA') ? getenv('DB_PG_SCHEMA') . '.' . $tableName : 'public' . $tableName; foreach ($groupadsResourceName as $data) { - $sql = "INSERT INTO {$tableName} - (ad_id, ad_group_id, campaign_id,customer_id, ad_name, status, resource_name) - VALUES (:ad_id, :ad_group_id,:campaign_id, :customer_id, :ad_name, :status, :resource_name) - ON CONFLICT (ad_id) - DO UPDATE SET - ad_group_id = EXCLUDED.ad_group_id, - campaign_id = EXCLUDED.campaign_id, - customer_id = EXCLUDED.customer_id, - ad_name = EXCLUDED.ad_name, - status = EXCLUDED.status, - resource_name = EXCLUDED.resource_name, - update_at = EXCLUDED.update_at"; -// dump($sql); - ThinkDb::execute($sql, $data); + // 构建 SQL 语句,添加 ad_type 和 metadata + $sql = "INSERT INTO {$tableName} + (ad_id, ad_group_id, campaign_id, customer_id, ad_name, status, resource_name, ad_type, metadata) + VALUES + (:ad_id, :ad_group_id, :campaign_id, :customer_id, :ad_name, :status, :resource_name, :ad_type, :metadata) + ON CONFLICT (ad_id) + DO UPDATE SET + ad_group_id = EXCLUDED.ad_group_id, + campaign_id = EXCLUDED.campaign_id, + customer_id = EXCLUDED.customer_id, + ad_name = EXCLUDED.ad_name, + status = EXCLUDED.status, + resource_name = EXCLUDED.resource_name, + ad_type = EXCLUDED.ad_type, + metadata = EXCLUDED.metadata, + update_at = EXCLUDED.update_at"; + + // 绑定数据 + $bindData = [ + 'ad_id' => $data['ad_id'], + 'ad_group_id' => $data['ad_group_id'], + 'campaign_id' => $data['campaign_id'], + 'customer_id' => $data['customer_id'], + 'ad_name' => $data['ad_name'], + 'status' => $data['status'], + 'resource_name' => $data['resource_name'], + 'ad_type' => $data['ad_type'], + 'metadata' => $data['metadata'], + ]; + + // 执行 SQL 插入语句 + ThinkDb::execute($sql, $bindData); } + } - - /** * Runs the example. * @@ -123,7 +140,8 @@ class GoogleAdsAdService extends BaseService */ // [START get_campaigns] - public static function getAds(GoogleAdsClient $googleAdsClient, int $customerId) + public + static function getAds(GoogleAdsClient $googleAdsClient, int $customerId) { $googleAdsServiceClient = $googleAdsClient->getGoogleAdsServiceClient(); // Creates a query that retrieves all groups. @@ -136,7 +154,13 @@ class GoogleAdsAdService extends BaseService customer.id, ad_group_ad.ad.name, ad_group_ad.status, - ad_group_ad.ad.resource_name + ad_group_ad.ad.resource_name, + ad_group_ad.ad.type, + ad_group_ad.ad.responsive_display_ad.logo_images, + ad_group_ad.ad.responsive_display_ad.square_logo_images, + ad_group_ad.ad.responsive_display_ad.marketing_images, + ad_group_ad.ad.responsive_display_ad.square_marketing_images, + ad_group_ad.ad.responsive_display_ad.youtube_videos FROM ad_group_ad WHERE ad_group_ad.status != 'REMOVED' "; @@ -154,6 +178,7 @@ class GoogleAdsAdService extends BaseService // $finalUrlsList = $googleAdsRow->getAdGroupAd()->getAd()->getFinalUrls(); // 将最终的 URL 列表转换为 PHP 数组 // $finalUrlsArray = iterator_to_array($finalUrlsList); + $resourceName = []; $resourceName['ad_id'] = $googleAdsRow->getAdGroupAd()->getAd()->getId(); $resourceName['ad_name'] = $googleAdsRow->getAdGroupAd()->getAd()->getName(); $resourceName['ad_group_id'] = $googleAdsRow->getAdGroup()->getId(); @@ -162,13 +187,51 @@ class GoogleAdsAdService extends BaseService // $resourceName['final_urls'] = $finalUrlsArray; $resourceName['status'] = $googleAdsRow->getAdGroupAd()->getStatus(); $resourceName['resource_name'] = $googleAdsRow->getAdGroupAd()->getAd()->getResourceName(); - $resourceNames[] = $resourceName; + $resourceName['ad_type'] = $googleAdsRow->getAdGroupAd()->getAd()->getType(); + //ad_type 19=> RESPONSIVE_DISPLAY_AD 自适应展示广告 详细看model定义 + if ($resourceName['ad_type'] === 19) { + // 获取 squareMarketingImages 中的每个 asset + $squareMarketingImages = $googleAdsRow->getAdGroupAd()->getAd()->getResponsiveDisplayAd()->getSquareMarketingImages(); + $squareMarketingAssets = []; + foreach ($squareMarketingImages as $image) { + $squareMarketingAssets[] = $image->getAsset(); + } + // 获取 logoImages 中的每个 asset + $logoImages = $googleAdsRow->getAdGroupAd()->getAd()->getResponsiveDisplayAd()->getLogoImages(); + $logoAssets = []; + foreach ($logoImages as $logo) { + $logoAssets[] = $logo->getAsset(); + } + // 获取 marketingImages 中的每个 asset + $marketingImages = $googleAdsRow->getAdGroupAd()->getAd()->getResponsiveDisplayAd()->getMarketingImages(); + $marketingAssets = []; + foreach ($marketingImages as $marketing) { + $marketingAssets[] = $marketing->getAsset(); + } + // 获取 youtubeVideos 中的每个 asset + $youtubeVideos = $googleAdsRow->getAdGroupAd()->getAd()->getResponsiveDisplayAd()->getYoutubeVideos(); + $youtubeAssets = []; + foreach ($youtubeVideos as $video) { + $youtubeAssets[] = $video->getAsset(); + } + // 将提取的资产保存到 metadata 数组 + $resourceName['metadata']['square_marketing_images'] = $squareMarketingAssets; + $resourceName['metadata']['logo_images'] = $logoAssets; + $resourceName['metadata']['marketing_images'] = $marketingAssets; + $resourceName['metadata']['youtube_videos'] = $youtubeAssets; + + // 将 metadata 转换为 JSON 格式 + $resourceName['metadata'] = json_encode($resourceName['metadata']); // 存储为 JSONB 格式 + } else { + $resourceName['metadata'] = null; + } + $resourceNames[] = $resourceName; + } return $resourceNames; } - /** * This example updates the CPC bid and status for a given ad group. To get ad groups, run * GetAdAds.php. @@ -178,7 +241,8 @@ class GoogleAdsAdService extends BaseService * @return mixed * @throws ApiException */ - public function runUpdateAd($options): mixed + public + function runUpdateAd($options): mixed { // $googleAdsClient = $this->googleAdsClient; $googleAdsClient = new GoogleAdsClientService($options['customer_id']); @@ -198,7 +262,8 @@ class GoogleAdsAdService extends BaseService * @param int $adGroupId the ad group ID that the ad group ad belongs to * @param int $adId the ID of the ad to pause */ - public static function updateAd( + public + static function updateAd( GoogleAdsClient $googleAdsClient, int $customerId, int $adGroupId, @@ -243,7 +308,8 @@ class GoogleAdsAdService extends BaseService /** * 更新广告状态 */ - public function updateAdStatus(int $customerId, int $adGroupId, int $adId, int $status) + public + function updateAdStatus(int $customerId, int $adGroupId, int $adId, int $status) { // 从数据库获取 Ad $ad = AdModel::find($adId); @@ -267,7 +333,8 @@ class GoogleAdsAdService extends BaseService /** * 获取广告状态 */ - public function getAdStatus(int $adId) + public + function getAdStatus(int $adId) { // 从数据库获取 Ad $ad = AdModel::find($adId); @@ -280,7 +347,6 @@ class GoogleAdsAdService extends BaseService } - /** * 判断广告是否启用 */ @@ -330,7 +396,8 @@ class GoogleAdsAdService extends BaseService * @return mixed * @throws ApiException */ - public function runGetResponsiveSearchAds($options): mixed + public + function runGetResponsiveSearchAds($options): mixed { // $googleAdsClient = $this->googleAdsClient; $googleAdsClient = new GoogleAdsClientService($options['customer_id']); @@ -351,7 +418,8 @@ class GoogleAdsAdService extends BaseService * @param int|null $adGroupId the ad group ID for which responsive search ads will be retrieved. * If `null`, returns from all ad groups */ - public static function getResponsiveSearchAds( + public + static function getResponsiveSearchAds( GoogleAdsClient $googleAdsClient, int $customerId, ?int $adGroupId @@ -421,7 +489,8 @@ class GoogleAdsAdService extends BaseService * @param RepeatedField $assets the list of AdTextAsset objects * @return string the string representation of the provided list of AdTextAsset objects */ - private static function convertAdTextAssetsToString(RepeatedField $assets): string + private + static function convertAdTextAssetsToString(RepeatedField $assets): string { $result = ''; foreach ($assets as $asset) { @@ -445,7 +514,8 @@ class GoogleAdsAdService extends BaseService * @return mixed * @throws ApiException */ - public function runUpdateResponsiveSearchAd($options): mixed + public + function runUpdateResponsiveSearchAd($options): mixed { // $googleAdsClient = $this->googleAdsClient; $googleAdsClient = new GoogleAdsClientService($options['customer_id']); @@ -463,7 +533,8 @@ class GoogleAdsAdService extends BaseService * @param int $adId the ad ID to update */ // [START update_responsive_search_ad] - public static function updateResponsiveSearchAd( + public + static function updateResponsiveSearchAd( GoogleAdsClient $googleAdsClient, int $customerId, int $adId @@ -514,6 +585,7 @@ class GoogleAdsAdService extends BaseService ); return $updatedAd->getResourceName(); } + // [END update_responsive_search_ad] @@ -525,7 +597,8 @@ class GoogleAdsAdService extends BaseService * @return mixed * @throws ApiException */ - public function runSearchForGoogleAdsFields($options): mixed + public + function runSearchForGoogleAdsFields($options): mixed { // $googleAdsClient = $this->googleAdsClient; $googleAdsClient = new GoogleAdsClientService($options['customer_id']); @@ -541,7 +614,8 @@ class GoogleAdsAdService extends BaseService * @param GoogleAdsClient $googleAdsClient the Google Ads API client * @param string $namePrefix the name prefix to use in the query */ - public static function searchForGoogleAdsFields(GoogleAdsClient $googleAdsClient, string $namePrefix) + public + static function searchForGoogleAdsFields(GoogleAdsClient $googleAdsClient, string $namePrefix) { $googleAdsFieldServiceClient = $googleAdsClient->getGoogleAdsFieldServiceClient(); // Searches for all fields whose name begins with the specified namePrefix. diff --git a/app/service/GoogleAdsAssetService.php b/app/service/GoogleAdsAssetService.php new file mode 100644 index 0000000..bfd1f31 --- /dev/null +++ b/app/service/GoogleAdsAssetService.php @@ -0,0 +1,741 @@ +where('advertiser_id', $advertiserId) // 根据 advertiser_id 查询 +// ->find(); // 获取第一个结果 +// +//// 如果找到广告主数据 +// if ($userAdvertiser && $userAdvertiser->googleUser) { +// // 获取关联用户的 access_token +// return $userAdvertiser->googleUser ? $userAdvertiser->googleUser->access_token : null; +// } else { +//// return $this->errorResponse('101', '未找到该广告主或关联的用户'); +// } +// } + + + /* @param int $customerId the customer ID + * @param $options + * @return mixed + * @throws ApiException + */ + public function runListAssets(int $customerId): mixed + { + + $googleAdsClient = new GoogleAdsClientService($customerId); + // Creates a single shared budget to be used by the campaigns added below. + $assetsResourceName = self::getAssets($googleAdsClient->getGoogleAdsClient(), $customerId); +// dump(json_encode($assetsResourceName)); + if (is_array($assetsResourceName)) { + self::saveAssets($assetsResourceName); + } + + return $assetsResourceName; + } + + + /* @param int $customerId the customer ID + * @param $options + * @return mixed + * @throws ApiException + */ + public function runListAds(int $customerId): mixed + { + +// $googleAdsClient = $this->googleAdsClient; + $googleAdsClient = new GoogleAdsClientService($customerId); + + // Creates a single shared budget to be used by the campaigns added below. + $groupAdsResourceName = self::getAds($googleAdsClient->getGoogleAdsClient(), $customerId); +// dump(json_encode($groupadsResourceName)); + if (is_array($groupAdsResourceName)) { + self::saveAds($groupAdsResourceName); + } + + return $groupAdsResourceName; + } + + + /** + * 在数据库中保存广告素材信息 + * @param $assetsResourceName + * @return void + */ + public static function saveAssets($assetsResourceName) + { + $tableName = 'bps_google_ads_asset'; + $tableName = getenv('DB_PG_SCHEMA') ? getenv('DB_PG_SCHEMA') . '.' . $tableName : 'public' . $tableName; + foreach ($assetsResourceName as $data) { + // 修改后的插入 SQL 语句 + $sql = "INSERT INTO {$tableName} + (asset_id, customer_id, asset_type, asset_name, resource_name, asset_url, status, metadata, create_at, update_at) + VALUES (:asset_id, :customer_id, :asset_type, :asset_name, :resource_name, :asset_url, :status, :metadata, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) + ON CONFLICT (asset_id) + DO UPDATE SET + customer_id = EXCLUDED.customer_id, + asset_type = EXCLUDED.asset_type, + asset_name = EXCLUDED.asset_name, + resource_name = EXCLUDED.resource_name, + asset_url = EXCLUDED.asset_url, + status = EXCLUDED.status, + metadata = EXCLUDED.metadata, + update_at = CURRENT_TIMESTAMP"; // update_at 使用 CURRENT_TIMESTAMP 自动更新 + + ThinkDb::execute($sql, $data); + } + } + + + + /** + * Runs the example. + * + * @param GoogleAdsClient $googleAdsClient the Google Ads API client + * @param int $customerId the customer ID + */ + // [START get_campaigns] + + public static function getAssets(GoogleAdsClient $googleAdsClient, int $customerId) + { + $googleAdsServiceClient = $googleAdsClient->getGoogleAdsServiceClient(); + // 查询广告素材及其关联的广告、广告组和广告系列 +// $query ="SELECT asset.id,asset.name, asset.image_asset.mime_type, asset.image_asset.full_size.url, customer.id FROM asset"; + $query = "SELECT asset.id, + asset.name, + asset.type, + asset.resource_name, + asset.source, + asset.image_asset.full_size.url, + asset.image_asset.mime_type, + asset.image_asset.file_size, + asset.image_asset.full_size.height_pixels, + asset.image_asset.full_size.width_pixels, + asset.youtube_video_asset.youtube_video_id, + asset.youtube_video_asset.youtube_video_title, + customer.id + FROM asset + WHERE + asset.type IN ('IMAGE', 'YOUTUBE_VIDEO') + LIMIT 10000"; + + // Issues a search stream request. + /** @var GoogleAdsServerStreamDecorator $stream */ + $stream = $googleAdsServiceClient->searchStream( + SearchGoogleAdsStreamRequest::build($customerId, $query) + ); + $resourceNames = []; + // Iterates over all rows in all messages and prints the requested field values for + // the campaign in each row. + foreach ($stream->iterateAllElements() as $googleAdsRow) { + /** @var GoogleAdsRow $googleAdsRow */ + // 将最终的 URL 列表转换为 PHP 数组 +// $finalUrlsArray = iterator_to_array($finalUrlsList); + $resourceName = []; + $resourceName['asset_type'] = $googleAdsRow->getAsset()->getType(); + if ($resourceName['asset_type'] === 2) { + $resourceName['asset_url'] = $googleAdsRow->getAsset()->getYoutubeVideoAsset()->getYoutubeVideoId(); + $resourceName['metadata'] ['youtube_video_id'] = $googleAdsRow->getAsset()->getYoutubeVideoAsset()->getYoutubeVideoId(); + $resourceName['metadata'] ['youtube_video_title'] = $googleAdsRow->getAsset()->getYoutubeVideoAsset()->getYoutubeVideoTitle(); + $resourceName['asset_name'] = $resourceName['metadata'] ['youtube_video_title']; + $resourceName['metadata'] = json_encode($resourceName['metadata']); + } elseif ($resourceName['asset_type'] === 4) { + $resourceName['asset_name'] = $googleAdsRow->getAsset()->getName(); + $resourceName['asset_url'] = $googleAdsRow->getAsset()->getImageAsset()->getFullSize()->getUrl(); + $resourceName['metadata'] ['mimeType'] = $googleAdsRow->getAsset()->getImageAsset()->getMimeType(); + $resourceName['metadata'] ['filesize'] = $googleAdsRow->getAsset()->getImageAsset()->getFileSize(); + $resourceName['metadata'] ['height_pixels'] = $googleAdsRow->getAsset()->getImageAsset()->getFullSize()->getHeightPixels(); + $resourceName['metadata'] ['width_pixels'] = $googleAdsRow->getAsset()->getImageAsset()->getFullSize()->getWidthPixels(); + $resourceName['metadata'] = json_encode($resourceName['metadata']); + } else { + continue; + } + $resourceName['asset_id'] = $googleAdsRow->getAsset()->getId(); + + $resourceName['status'] = 2; //未定义,先占坑 + $resourceName['resource_name'] = $googleAdsRow->getAsset()->getResourceName(); + $resourceName['customer_id'] = $googleAdsRow->getCustomer()->getId(); + + $resourceNames[] = $resourceName; + } + return $resourceNames; + } + + + + + /** + * Runs the example. + * + * @param GoogleAdsClient $googleAdsClient the Google Ads API client + * @param int $customerId the customer ID + */ + // [START get_campaigns] + + public static function getAds(GoogleAdsClient $googleAdsClient, int $customerId) + { + $googleAdsServiceClient = $googleAdsClient->getGoogleAdsServiceClient(); + // Creates a query that retrieves all groups. + +// $response = $googleAdsServiceClient->search($customerId, $query); + $query = "SELECT + ad_group_ad.ad.id, + ad_group.id, + campaign.id, + customer.id, + ad_group_ad.ad.name, + ad_group_ad.status, + ad_group_ad.ad.resource_name + FROM ad_group_ad + WHERE + ad_group_ad.status != 'REMOVED' "; + // Issues a search stream request. + /** @var GoogleAdsServerStreamDecorator $stream */ + $stream = $googleAdsServiceClient->searchStream( + SearchGoogleAdsStreamRequest::build($customerId, $query) + ); + $resourceNames = []; + // Iterates over all rows in all messages and prints the requested field values for + // the campaign in each row. + foreach ($stream->iterateAllElements() as $googleAdsRow) { + /** @var GoogleAdsRow $googleAdsRow */ + // 假设 $googleAdsRow 是从 Google Ads API 中获取的对象 +// $finalUrlsList = $googleAdsRow->getAdGroupAd()->getAd()->getFinalUrls(); + // 将最终的 URL 列表转换为 PHP 数组 +// $finalUrlsArray = iterator_to_array($finalUrlsList); + $resourceName['ad_id'] = $googleAdsRow->getAdGroupAd()->getAd()->getId(); + $resourceName['ad_name'] = $googleAdsRow->getAdGroupAd()->getAd()->getName(); + $resourceName['ad_group_id'] = $googleAdsRow->getAdGroup()->getId(); + $resourceName['campaign_id'] = $googleAdsRow->getCampaign()->getId(); + $resourceName['customer_id'] = $googleAdsRow->getCustomer()->getId(); +// $resourceName['final_urls'] = $finalUrlsArray; + $resourceName['status'] = $googleAdsRow->getAdGroupAd()->getStatus(); + $resourceName['resource_name'] = $googleAdsRow->getAdGroupAd()->getAd()->getResourceName(); + $resourceNames[] = $resourceName; + } + return $resourceNames; + } + + + + /** + * This example updates the CPC bid and status for a given ad group. To get ad groups, run + * GetAdAds.php. + */ + /* @param int $customerId the customer ID + * @param $options + * @return mixed + * @throws ApiException + */ + public function runUpdateAd($options): mixed + { +// $googleAdsClient = $this->googleAdsClient; + $googleAdsClient = new GoogleAdsClientService($options['customer_id']); + // Creates a single shared budget to be used by the campaigns added below. + + $resourceNames = self::updateAd($googleAdsClient->getGoogleAdsClient(), $options['customer_id'], $options['group_id'], $options['ad_id'], $options['status']); + + return $resourceNames; + } + + + /** + * Runs the updateAd example. + * + * @param GoogleAdsClient $googleAdsClient the Google Ads API client + * @param int $customerId the customer ID + * @param int $adGroupId the ad group ID that the ad group ad belongs to + * @param int $adId the ID of the ad to pause + */ + public static function updateAd( + GoogleAdsClient $googleAdsClient, + int $customerId, + int $adGroupId, + int $adId, + int $status + ) + { + // Creates ad group ad resource name. + $adGroupAdResourceName = ResourceNames::forAdGroupAd($customerId, $adGroupId, $adId); + + // Creates an ad and sets its status to PAUSED. + $adGroupAd = new AdGroupAd(); + $adGroupAd->setResourceName($adGroupAdResourceName); +// $adGroupAd->setStatus(AdGroupAdStatus::PAUSED); + $adGroupAd->setStatus($status); + + // Constructs an operation that will pause the ad with the specified resource name, + // using the FieldMasks utility to derive the update mask. This mask tells the Google Ads + // API which attributes of the ad group you want to change. + $adGroupAdOperation = new AdGroupAdOperation(); + $adGroupAdOperation->setUpdate($adGroupAd); + $adGroupAdOperation->setUpdateMask(FieldMasks::allSetFieldsOf($adGroupAd)); + + // Issues a mutate request to pause the ad group ad. + $adGroupAdServiceClient = $googleAdsClient->getAdGroupAdServiceClient(); + $response = $adGroupAdServiceClient->mutateAdGroupAds(MutateAdGroupAdsRequest::build( + $customerId, + [$adGroupAdOperation] + )); + + // Prints the resource name of the paused ad group ad. + /** @var AdGroupAd $pausedAdGroupAd */ + $pausedAdGroupAd = $response->getResults()[0]; +// printf( +// "Ad group ad with resource name: '%s' is paused.%s", +// $pausedAdGroupAd->getResourceName(), +// PHP_EOL +// ); + return $pausedAdGroupAd->getResourceName(); + } + + /** + * 更新广告状态 + */ + public function updateAdStatus(int $customerId, int $adGroupId, int $adId, int $status) + { + // 从数据库获取 Ad + $ad = AdModel::find($adId); + if (!$ad) { +// throw new ValidateException('Ad not found'); + return false; + } + // 更新数据库中的状态 +// $ad->updateStatus($status); + if ($this->modifyDbAdStatus($adId, $status)) { + // 更新 Google Ads 上的状态 +// $googleAdsClient = $this->googleAdsClient; + $googleAdsClient = new GoogleAdsClientService($customerId); + $resourceName = self::updateAd($googleAdsClient->getGoogleAdsClient(), $customerId, $adGroupId, $adId, $status); + return true; + } + + return false; + } + + /** + * 获取广告状态 + */ + public function getAdStatus(int $adId) + { + // 从数据库获取 Ad + $ad = AdModel::find($adId); + if (!$ad) { +// throw new ValidateException('Ad not found'); + } + + // 返回广告状态 + return $ad->getStatusTextAttr(null, $ad->toArray()); + } + + + + /** + * 判断广告是否启用 + */ +// public function isAdEnabled(int $adId) +// { +// $ad = Ad::find($adId); +// if (!$ad) { +// throw new ValidateException('Ad not found'); +// } +// +// return $ad->isEnabled(); +// } + + /** + * 判断广告是否暂停 + */ +// public function isAdPaused(int $adId) +// { +// $ad = Ad::find($adId); +// if (!$ad) { +// throw new ValidateException('Ad not found'); +// } +// +// return $ad->isPaused(); +// } + + /** + * 判断广告是否停止 + */ +// public function isAdStopped(int $adId) +// { +// $ad = Ad::find($adId); +// if (!$ad) { +// throw new ValidateException('Ad not found'); +// } +// +// return $ad->isStopped(); +// } + + + /** + * This example updates the CPC bid and status for a given ad group. To get ad groups, run + * GetAdAds.php. + */ + /* @param int $customerId the customer ID + * @param $options + * @return mixed + * @throws ApiException + */ + public function runGetResponsiveSearchAds($options): mixed + { +// $googleAdsClient = $this->googleAdsClient; + $googleAdsClient = new GoogleAdsClientService($options['customer_id']); + // Creates a single shared budget to be used by the campaigns added below. + if (!isset($options['group_id'])) { + $options['group_id'] = null; + } + $resourceNames = self::getResponsiveSearchAds($googleAdsClient->getGoogleAdsClient(), $options['customer_id'], $options['group_id']); + + return $resourceNames; + } + + /** + * 获取指定广告组中未移除的自适应搜索广告。 + * + * @param GoogleAdsClient $googleAdsClient the Google Ads API client + * @param int $customerId the customer ID + * @param int|null $adGroupId the ad group ID for which responsive search ads will be retrieved. + * If `null`, returns from all ad groups + */ + public static function getResponsiveSearchAds( + GoogleAdsClient $googleAdsClient, + int $customerId, + ?int $adGroupId + ) + { + $googleAdsServiceClient = $googleAdsClient->getGoogleAdsServiceClient(); + + // Creates a query that retrieves responsive search ads. + $query = + 'SELECT ad_group.id, ' + . 'ad_group_ad.ad.id, ' + . 'ad_group_ad.ad.responsive_search_ad.headlines, ' + . 'ad_group_ad.ad.responsive_search_ad.descriptions, ' + . 'ad_group_ad.status ' + . 'FROM ad_group_ad ' + . 'WHERE ad_group_ad.ad.type = RESPONSIVE_SEARCH_AD ' + . 'AND ad_group_ad.status != "REMOVED"'; + if (!is_null($adGroupId)) { + $query .= " AND ad_group.id = $adGroupId"; + } + + // Issues a search request. + $response = + $googleAdsServiceClient->search(SearchGoogleAdsRequest::build($customerId, $query)); + + // Iterates over all rows in all pages and prints the requested field values for + // the responsive search ad in each row. + $isEmptyResult = true; + $resources = []; + foreach ($response->iterateAllElements() as $googleAdsRow) { + /** @var GoogleAdsRow $googleAdsRow */ + $isEmptyResult = false; + $ad = $googleAdsRow->getAdGroupAd()->getAd(); + $resource = []; +// printf( +// "Responsive search ad with resource name '%s' and status '%s' was found.%s", +// $ad->getResourceName(), +// AdGroupAdStatus::name($googleAdsRow->getAdGroupAd()->getStatus()), +// PHP_EOL +// ); + $resource['resource_name'] = $ad->getResourceName(); + $resource['status'] = AdGroupAdStatus::name($googleAdsRow->getAdGroupAd()->getStatus()); + $responsiveSearchAdInfo = $ad->getResponsiveSearchAd(); +// printf( +// 'Headlines:%1$s%2$sDescriptions:%1$s%3$s%1$s', +// PHP_EOL, +// self::convertAdTextAssetsToString($responsiveSearchAdInfo->getHeadlines()), +// self::convertAdTextAssetsToString($responsiveSearchAdInfo->getDescriptions()) +// ) + $resource['content'] = sprintf( + 'Headlines:%1$s%2$sDescriptions:%1$s%3$s%1$s', + PHP_EOL, + self::convertAdTextAssetsToString($responsiveSearchAdInfo->getHeadlines()), + self::convertAdTextAssetsToString($responsiveSearchAdInfo->getDescriptions()) + ); + $resources[] = $resource; + } + return $resources; +// if ($isEmptyResult) { +// print 'No responsive search ads were found.' . PHP_EOL; +// } + } + + /** + * Converts the list of AdTextAsset objects into a string representation. + * + * @param RepeatedField $assets the list of AdTextAsset objects + * @return string the string representation of the provided list of AdTextAsset objects + */ + private static function convertAdTextAssetsToString(RepeatedField $assets): string + { + $result = ''; + foreach ($assets as $asset) { + /** @var AdTextAsset $asset */ + $result .= sprintf( + "\t%s pinned to %s.%s", + $asset->getText(), + ServedAssetFieldType::name($asset->getPinnedField()), + PHP_EOL + ); + } + return $result; + } + + /** + * This example updates the CPC bid and status for a given ad group. To get ad groups, run + * GetAdAds.php. + */ + /* @param int $customerId the customer ID + * @param $options + * @return mixed + * @throws ApiException + */ + public function runUpdateResponsiveSearchAd($options): mixed + { +// $googleAdsClient = $this->googleAdsClient; + $googleAdsClient = new GoogleAdsClientService($options['customer_id']); + // Creates a single shared budget to be used by the campaigns added below. + $resourceName = self::updateResponsiveSearchAd($googleAdsClient->getGoogleAdsClient(), $options['customer_id'], $options['ad_id']); + + return $resourceName; + } + + /** + * updateResponsiveSearchAd. + * + * @param GoogleAdsClient $googleAdsClient the Google Ads API client + * @param int $customerId the customer ID + * @param int $adId the ad ID to update + */ + // [START update_responsive_search_ad] + public static function updateResponsiveSearchAd( + GoogleAdsClient $googleAdsClient, + int $customerId, + int $adId + ) + { + // Creates an ad with the specified resource name and other changes. + $ad = new Ad([ + 'resource_name' => ResourceNames::forAd($customerId, $adId), + 'responsive_search_ad' => new ResponsiveSearchAdInfo([ + // Update some properties of the responsive search ad. + 'headlines' => [ + new AdTextAsset([ + 'text' => 'Cruise to Pluto #' . Helper::getShortPrintableDatetime(), + 'pinned_field' => ServedAssetFieldType::HEADLINE_1 + ]), + new AdTextAsset(['text' => 'Tickets on sale now', 'pinned_field' => ServedAssetFieldType::HEADLINE_2]), + new AdTextAsset(['text' => 'Buy your ticket now']) + ], + 'descriptions' => [ + new AdTextAsset(['text' => 'Best space cruise ever.']), + new AdTextAsset([ + 'text' => 'The most wonderful space experience you will ever have.']) + ] + ]), + 'final_urls' => ['http://www.baidu.com'], + 'final_mobile_urls' => ['http://www.baidu.com/mobile'] + ]); + + // Constructs an operation that will update the ad, using the FieldMasks to derive the + // update mask. This mask tells the Google Ads API which attributes of the ad you want to + // change. + $adOperation = new AdOperation(); + $adOperation->setUpdate($ad); + $adOperation->setUpdateMask(FieldMasks::allSetFieldsOf($ad)); + + // Issues a mutate request to update the ad. + $adServiceClient = $googleAdsClient->getAdServiceClient(); + $response = + $adServiceClient->mutateAds(MutateAdsRequest::build($customerId, [$adOperation])); + + // Prints the resource name of the updated ad. + /** @var Ad $updatedAd */ + $updatedAd = $response->getResults()[0]; + printf( + "Updated ad with resource name: '%s'.%s", + $updatedAd->getResourceName(), + PHP_EOL + ); + return $updatedAd->getResourceName(); + } + // [END update_responsive_search_ad] + + + /** + * 根据给定的前缀搜索 Google Ads 字段,检索到关于这些字段的元数据(metadata) + */ + /* @param int $customerId the customer ID + * @param $options + * @return mixed + * @throws ApiException + */ + public function runSearchForGoogleAdsFields($options): mixed + { +// $googleAdsClient = $this->googleAdsClient; + $googleAdsClient = new GoogleAdsClientService($options['customer_id']); + // Creates a single shared budget to be used by the campaigns added below. + $googleAdsFieldData = self::searchForGoogleAdsFields($googleAdsClient->getGoogleAdsClient(), $options['name_prefix']); + + return $googleAdsFieldData; + } + + /** + * Runs the example SearchForGoogleAdsFields. + * + * @param GoogleAdsClient $googleAdsClient the Google Ads API client + * @param string $namePrefix the name prefix to use in the query + */ + public static function searchForGoogleAdsFields(GoogleAdsClient $googleAdsClient, string $namePrefix) + { + $googleAdsFieldServiceClient = $googleAdsClient->getGoogleAdsFieldServiceClient(); + // Searches for all fields whose name begins with the specified namePrefix. + // A single "%" is the wildcard token in the Google Ads Query language. + $query = "SELECT name, category, selectable, filterable, sortable, selectable_with, " + . "data_type, is_repeated WHERE name LIKE '$namePrefix%'"; + $response = $googleAdsFieldServiceClient->searchGoogleAdsFields( + SearchGoogleAdsFieldsRequest::build($query) + ); + + if (iterator_count($response->getIterator()) === 0) { + printf( + "No GoogleAdsFields found with a name that begins with %s.%s", + $namePrefix, + PHP_EOL + ); + return; + } + // Iterates over all rows and prints our the metadata of each matching GoogleAdsField. + foreach ($response->iterateAllElements() as $googleAdsField) { + /** @var GoogleAdsField $googleAdsField */ + $fieldInfo = [ + 'name' => $googleAdsField->getName(), + 'category' => GoogleAdsFieldCategory::name($googleAdsField->getCategory()), + 'data_type' => GoogleAdsFieldDataType::name($googleAdsField->getDataType()), + 'selectable' => $googleAdsField->getSelectable() ? 'true' : 'false', + 'filterable' => $googleAdsField->getFilterable() ? 'true' : 'false', + 'sortable' => $googleAdsField->getSortable() ? 'true' : 'false', + 'repeated' => $googleAdsField->getIsRepeated() ? 'true' : 'false', + 'selectable_with' => [] + ]; + // Check if there are fields that are selectable with the current field + if ($googleAdsField->getSelectableWith()->count() > 0) { + $selectableWithFields = iterator_to_array($googleAdsField->getSelectableWith()->getIterator()); + sort($selectableWithFields); // Sort the fields alphabetically + $fieldInfo['selectable_with'] = $selectableWithFields; + } + + // Add the field info to the result array + $googleAdsFieldData[] = $fieldInfo; +// +// printf("%s:%s", $googleAdsField->getName(), PHP_EOL); +// printf( +// " %-16s: %s%s", +// "category:", +// GoogleAdsFieldCategory::name($googleAdsField->getCategory()), +// PHP_EOL +// ); +// printf( +// " %-16s: %s%s", +// "data type:", +// GoogleAdsFieldDataType::name($googleAdsField->getDataType()), +// PHP_EOL +// ); +// printf( +// " %-16s: %s%s", +// "selectable:", +// $googleAdsField->getSelectable() ? 'true' : 'false', +// PHP_EOL +// ); +// printf( +// " %-16s: %s%s", +// "filterable:", +// $googleAdsField->getFilterable() ? 'true' : 'false', +// PHP_EOL +// ); +// printf( +// " %-16s: %s%s", +// "sortable:", +// $googleAdsField->getSortable() ? 'true' : 'false', +// PHP_EOL +// ); +// printf( +// " %-16s: %s%s", +// "repeated:", +// $googleAdsField->getIsRepeated() ? 'true' : 'false', +// PHP_EOL +// ); +// +// if ($googleAdsField->getSelectableWith()->count() > 0) { +// // Prints the list of fields that are selectable with the field. +// $selectableWithFields = +// iterator_to_array($googleAdsField->getSelectableWith()->getIterator()); +// // Sorts and then prints the list. +// sort($selectableWithFields); +// print ' selectable with:' . PHP_EOL; +// foreach ($selectableWithFields as $selectableWithField) { +// /** @var string $selectableWithField */ +// printf(" $selectableWithField%s", PHP_EOL); +// } +// } + } + + return $googleAdsFieldData; // Return the result array + } + + +} diff --git a/config/event.php b/config/event.php index 962ef2a..b5d6668 100644 --- a/config/event.php +++ b/config/event.php @@ -6,6 +6,7 @@ use app\event\TiktokAdsDetails; use app\event\GoogleAdsCampaigns; use app\event\GoogleAdsGroups; use app\event\GoogleAdsAds; +use app\event\GoogleAdsAssets; use app\event\GoogleAdsDateDatas; @@ -29,6 +30,9 @@ return [ GoogleAdsAds::type => [ [GoogleAdsAds::class, 'getAds'], ], + GoogleAdsAssets::type => [ + [GoogleAdsAssets::class, 'getAssets'], + ], ]; diff --git a/config/route.php b/config/route.php index 55f0d58..a8f40fa 100644 --- a/config/route.php +++ b/config/route.php @@ -100,6 +100,9 @@ Route::group('/googleads', function () { Route::group('/group', function () { Route::post('/list', [GoogleAdsController::class, 'listGroups']); }); + Route::group('/asset', function () { + Route::post('/list', [GoogleAdsController::class, 'listAssets']); + }); Route::group('/ad', function () { Route::post('/update', [GoogleAdsController::class, 'updateAd']); Route::post('/list', [GoogleAdsController::class, 'listAds']);