diff --git a/app/controller/GoogleAdsController.php b/app/controller/GoogleAdsController.php index c4e79e7..e7380cc 100644 --- a/app/controller/GoogleAdsController.php +++ b/app/controller/GoogleAdsController.php @@ -10,6 +10,7 @@ use app\service\GoogleAdsCampaignService; use app\service\GoogleAdsGroupService; use app\service\GoogleAdsAdService; use app\service\GoogleAdsAssetService; +use app\service\GoogleAdsAssetRelationService; use app\service\GoogleAdsAccountService; use support\Response; use DI\Annotation\Inject; @@ -42,6 +43,12 @@ class GoogleAdsController */ private $googleAdsAssetService; + /** + * @Inject + * @var GoogleAdsAssetRelationService + */ + private $googleAdsAssetRelationService; + /** * @Inject * @var GoogleAdsAccountService @@ -93,6 +100,17 @@ class GoogleAdsController // 继续处理 Google Ads API 操作 return $this->getAssets($options); } + public function listAssetRelations(Request $request) + { + $options = $request->all(); + + // 继续处理 Google Ads API 操作 + if($options['asset_type'] == 2){ + return $this->getVideoAssetRelations($options); + }elseif($options['asset_type'] == 4){ + $this->getAssetRelations($options); + } + } public function listDateDatas(Request $request) { @@ -279,6 +297,27 @@ class GoogleAdsController // return $this->successResponse(['assets_list' => $resourceName]); return $this->successResponse(['assets_list' => 'succeed added']); } + /** + * get assets + * @throws ApiException + */ + public function getAssetRelations($options): Response + { + $resourceName = $this->googleAdsAssetRelationService->runListAssetRelations($options['customer_id']); + return $this->successResponse(['assets_relation_list' => $resourceName]); +// return $this->successResponse(['assets_relation_list' => 'succeed added']); + } + + /** + * get assets + * @throws ApiException + */ + public function getVideoAssetRelations($options): Response + { + $resourceName = $this->googleAdsAssetRelationService->runListVideoAssetRelations($options['customer_id']); + return $this->successResponse(['assets_relation_list' => $resourceName]); +// return $this->successResponse(['assets_relation_list' => 'succeed added']); + } /** * get date datas diff --git a/app/event/GoogleAdsAssetRelations.php b/app/event/GoogleAdsAssetRelations.php new file mode 100644 index 0000000..b6d2e93 --- /dev/null +++ b/app/event/GoogleAdsAssetRelations.php @@ -0,0 +1,297 @@ +all(); + + // 继续处理 Google Ads API 操作 + return $this->getAssetRelations($options); + } + + /** + * get asset relations + * @throws ApiException + */ + public function getAssetRelations($options) + { + $customers = $this->googleOAuthService->getGoogleAdCustomers([]); + foreach ($customers as $customerId) { + $googleAdsAssetRelationService = new GoogleAdsAssetRelationService($customerId); + $resourceName = $googleAdsAssetRelationService->runListAssetRelations($customerId); + } + +// return $this->successResponse(['ads_list' => $resourceName]); + } + /** + * get asset relations + * @throws ApiException + */ + public function getVideoAssetRelations($options) + { + $customers = $this->googleOAuthService->getGoogleAdCustomers([]); + foreach ($customers as $customerId) { + $googleAdsAssetRelationService = new GoogleAdsAssetRelationService($customerId); + $resourceName = $googleAdsAssetRelationService->runListVideoAssetRelations($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 f41910f..b6661e1 100644 --- a/app/model/Ad.php +++ b/app/model/Ad.php @@ -25,6 +25,11 @@ class Ad extends Model 'ad_group_id' => 'int', 'customer_id' => 'int', ]; + + // 设置json类型字段 + protected $json = ['metadata']; + + // 默认值设置 protected $defaults = [ 'status' => 1, // 广告状态默认值为 'ENABLED' diff --git a/app/process/UpdateGoogleAdsTask.php b/app/process/UpdateGoogleAdsTask.php index 36ca56d..2f5570b 100644 --- a/app/process/UpdateGoogleAdsTask.php +++ b/app/process/UpdateGoogleAdsTask.php @@ -7,6 +7,7 @@ use app\event\GoogleAdsCampaigns; use app\event\GoogleAdsGroups; use app\event\GoogleAdsAds; use app\event\GoogleAdsAssets; +use app\event\GoogleAdsAssetRelations; use app\event\GoogleAdsDateDatas; use Webman\Event\Event; use Workerman\Crontab\Crontab; @@ -24,33 +25,33 @@ class UpdateGoogleAdsTask // 每15分钟执行一次 new Crontab('10 */15 * * * *', function () { - $dayBeforeYesterdayStart = date('Y-m-d', strtotime('-2 day')); - dump($dayBeforeYesterdayStart . '更新' . GoogleAdsDateDatas::type . '开始'); - Event::emit(GoogleAdsDateDatas::type, ['date' => $dayBeforeYesterdayStart]); +// $dayBeforeYesterdayStart = date('Y-m-d', strtotime('-2 day')); +// dump($dayBeforeYesterdayStart . '更新' . GoogleAdsDateDatas::type . '开始'); +// Event::emit(GoogleAdsDateDatas::type, ['date' => $dayBeforeYesterdayStart]); } ); // 每15分钟执行一次 new Crontab('20 */15 * * * *', function () { - $yesterdayStart = date('Y-m-d', strtotime('-1 day')); - dump($yesterdayStart . '更新' . GoogleAdsDateDatas::type . '开始'); - Event::emit(GoogleAdsDateDatas::type, ['date' => $yesterdayStart]); +// $yesterdayStart = date('Y-m-d', strtotime('-1 day')); +// dump($yesterdayStart . '更新' . GoogleAdsDateDatas::type . '开始'); +// Event::emit(GoogleAdsDateDatas::type, ['date' => $yesterdayStart]); } ); // 每15分钟执行一次 new Crontab('30 */15 * * * *', function () { //获取今天的 0 点的YYYY-MM-DD格式 - $todayStart = date('Y-m-d', strtotime('0 day')); - dump($todayStart . '更新' . GoogleAdsDateDatas::type . '开始'); - Event::emit(GoogleAdsDateDatas::type, ['date' => $todayStart]); +// $todayStart = date('Y-m-d', strtotime('0 day')); +// dump($todayStart . '更新' . GoogleAdsDateDatas::type . '开始'); +// Event::emit(GoogleAdsDateDatas::type, ['date' => $todayStart]); } ); // 每15分钟执行一次 new Crontab('40 */15 * * * *', function () { - dump(date('Y-m-d H:i:s') . '更新' . GoogleAdsCampaigns::type . '开始'); - Event::emit(GoogleAdsCampaigns::type, []); +// dump(date('Y-m-d H:i:s') . '更新' . GoogleAdsCampaigns::type . '开始'); +// Event::emit(GoogleAdsCampaigns::type, []); } ); @@ -63,15 +64,26 @@ class UpdateGoogleAdsTask // 每15分钟执行一次 new Crontab('55 */15 * * * *', function () { - 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(GoogleAdsAds::type, []); } ); // 每15分钟执行一次 - new Crontab('58 */15 * * * *', function () { - dump(date('Y-m-d H:i:s') . '更新' . GoogleAdsAssets::type . '开始'); - Event::emit(GoogleAdsAssets::type, []); + new Crontab('25 */15 * * * *', function () { +// dump(date('Y-m-d H:i:s') . '更新' . GoogleAdsAssets::type . '开始'); +// Event::emit(GoogleAdsAssets::type, []); + } + ); + + new Crontab('55 */50 * * * *', function () { + dump(date('Y-m-d H:i:s') . '更新' . GoogleAdsAssetRelations::IMAGEASSET . '开始'); + Event::emit(GoogleAdsAssetRelations::IMAGEASSET, []); + }); + + new Crontab('55 */51 * * * *', function () { + dump(date('Y-m-d H:i:s') . '更新' . GoogleAdsAssetRelations::VIDEOASSET . '开始'); + Event::emit(GoogleAdsAssetRelations::VIDEOASSET, []); } ); diff --git a/app/service/GoogleAdsAssetRelationService.php b/app/service/GoogleAdsAssetRelationService.php new file mode 100644 index 0000000..5ebff6f --- /dev/null +++ b/app/service/GoogleAdsAssetRelationService.php @@ -0,0 +1,679 @@ +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 runListAssetRelations(int $customerId): mixed + { + // Creates a single shared budget to be used by the campaigns added below. + $assetsResourceName = self::getAssetRelations($customerId); +// dump(json_encode($assetsResourceName)); + if (is_array($assetsResourceName)) { + self::saveAssetRelations($assetsResourceName); + } +// return $assetsResourceName; + return 'insert success'; + } + + + /* @param int $customerId the customer ID + * @param $options + * @return mixed + * @throws ApiException + */ + public function runListVideoAssetRelations(int $customerId): mixed + { + // Creates a single shared budget to be used by the campaigns added below. + $assetsResourceName = self::getVideoAssetRelations($customerId); +// dump(json_encode($assetsResourceName)); + if (is_array($assetsResourceName)) { + self::saveAssetRelations($assetsResourceName); + } +// return $assetsResourceName; + return 'insert success'; + } + + + /** + * 在数据库中保存广告素材信息 + * @param $assetsResourceName + * @return void + */ + public static function saveAssetRelations($assetsResourceName) + { + $tableName = 'bps_google_ads_asset_relations'; + $tableName = getenv('DB_PG_SCHEMA') ? getenv('DB_PG_SCHEMA') . '.' . $tableName : 'public' . $tableName; + foreach ($assetsResourceName as $data) { + // 修改后的插入 SQL 语句 + $sql = "INSERT INTO {$tableName} + (asset_id, ad_id, ad_group_id, campaign_id, date, create_at, update_at) + VALUES (:asset_id, :ad_id, :ad_group_id, :campaign_id, :date, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) + ON CONFLICT (asset_id, ad_id, date) + DO NOTHING"; // 如果 (asset_id, ad_id, date) 存在,忽略插入操作 + + ThinkDb::execute($sql, $data); + } + } + + + /** + * + * @param int $customerId the customer ID + */ + + public static function getAssetRelations(int $customerId) + { + // 获取所有素材 +// $assets = AssetModel::where('asset_type', 4)->select(); //图片素材 + + $resourceNames = AssetModel::where('asset_type', 4) + ->column('asset_id, resource_name'); + +// dump($resourceNames);return($resourceNames); + $result = []; + foreach ($resourceNames as $resourceName) { + // 获取广告表中的所有广告 + $ads = ThinkDb::table('bps.bps_google_ads_ad') +// ->whereRaw("metadata->'marketing_images' @> ?", ['["customers/4060397299/assets/191677352383"]']) +// ->whereOrRaw("metadata->'square_marketing_images' @> ?", ['["customers/4060397299/assets/191677352383"]']) + ->whereRaw("metadata->'marketing_images' @> ?", ['["' . $resourceName['resource_name'] . '"]']) + ->whereOrRaw("metadata->'square_marketing_images' @> ?", ['["' . $resourceName['resource_name'] . '"]']) + ->select(); + +// $ads = AdModel::where(function ($query) use ($resourceName) { +// $query->whereJsonContains('metadata->marketing_images', $resourceName) +// ->WhereOrJsonContains('metadata->square_marketing_images', $resourceName); +// })->select(); +// $result[$resourceName['asset_id']] = $ads; + + foreach ($ads as $ad) { + $result[$resourceName['asset_id']]['ad_id'] = $ad['ad_id']; + $result[$resourceName['asset_id']]['ad_group_id'] = $ad['ad_group_id']; + $result[$resourceName['asset_id']]['campaign_id'] = $ad['campaign_id']; + $result[$resourceName['asset_id']]['asset_id'] = $resourceName['asset_id']; + $result[$resourceName['asset_id']]['date'] = date('Y-m-d'); + } + + } + return $result; +// dump('Google Ads Asset synchronization completed.'); + + } + + /** + * + * @param int $customerId the customer ID + */ + + public static function getVideoAssetRelations(int $customerId) + { + // 获取所有素材 +// $assets = AssetModel::where('asset_type', 2)->select(); //视频素材 + + $resourceNames = AssetModel::where('asset_type', 2) + ->column('asset_id, resource_name'); + +// dump($resourceNames);return($resourceNames); + $result = []; + foreach ($resourceNames as $resourceName) { + // 获取广告表中的所有匹配的广告 + $ads = ThinkDb::table('bps.bps_google_ads_ad') + ->whereRaw("metadata->'youtube_videos' @> ?", ['["' . $resourceName['resource_name'] . '"]']) + ->select(); + foreach ($ads as $ad) { + $result[$resourceName['asset_id']]['ad_id'] = $ad['ad_id']; + $result[$resourceName['asset_id']]['ad_group_id'] = $ad['ad_group_id']; + $result[$resourceName['asset_id']]['campaign_id'] = $ad['campaign_id']; + $result[$resourceName['asset_id']]['asset_id'] = $resourceName['asset_id']; + $result[$resourceName['asset_id']]['date'] = date('Y-m-d'); + } + + } + return $result; +// dump('Google Ads Asset synchronization completed.'); + + } + + + /** + * 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/app/service/GoogleAdsAssetService.php b/app/service/GoogleAdsAssetService.php index bfd1f31..f2d699e 100644 --- a/app/service/GoogleAdsAssetService.php +++ b/app/service/GoogleAdsAssetService.php @@ -119,8 +119,8 @@ class GoogleAdsAssetService extends BaseService 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) + (asset_id, customer_id, asset_type, asset_name, resource_name, asset_url, status, asset_source,metadata, create_at, update_at) + VALUES (:asset_id, :customer_id, :asset_type, :asset_name, :resource_name, :asset_url, :status,:source, :metadata, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) ON CONFLICT (asset_id) DO UPDATE SET customer_id = EXCLUDED.customer_id, @@ -129,6 +129,7 @@ class GoogleAdsAssetService extends BaseService resource_name = EXCLUDED.resource_name, asset_url = EXCLUDED.asset_url, status = EXCLUDED.status, + asset_source = EXCLUDED.asset_source, metadata = EXCLUDED.metadata, update_at = CURRENT_TIMESTAMP"; // update_at 使用 CURRENT_TIMESTAMP 自动更新 @@ -204,6 +205,7 @@ class GoogleAdsAssetService extends BaseService $resourceName['status'] = 2; //未定义,先占坑 $resourceName['resource_name'] = $googleAdsRow->getAsset()->getResourceName(); + $resourceName['source'] = $googleAdsRow->getAsset()->getSource(); $resourceName['customer_id'] = $googleAdsRow->getCustomer()->getId(); $resourceNames[] = $resourceName; diff --git a/config/event.php b/config/event.php index b5d6668..81b80a1 100644 --- a/config/event.php +++ b/config/event.php @@ -7,6 +7,7 @@ use app\event\GoogleAdsCampaigns; use app\event\GoogleAdsGroups; use app\event\GoogleAdsAds; use app\event\GoogleAdsAssets; +use app\event\GoogleAdsAssetRelations; use app\event\GoogleAdsDateDatas; @@ -33,6 +34,12 @@ return [ GoogleAdsAssets::type => [ [GoogleAdsAssets::class, 'getAssets'], ], + GoogleAdsAssetRelations::IMAGEASSET => [ + [GoogleAdsAssetRelations::class, 'getAssetRelations'], + ], + GoogleAdsAssetRelations::VIDEOASSET => [ + [GoogleAdsAssetRelations::class, 'getVideoAssetRelations'], + ], ]; diff --git a/config/route.php b/config/route.php index a8f40fa..35206ae 100644 --- a/config/route.php +++ b/config/route.php @@ -103,6 +103,9 @@ Route::group('/googleads', function () { Route::group('/asset', function () { Route::post('/list', [GoogleAdsController::class, 'listAssets']); }); + Route::group('/asset_relation', function () { + Route::post('/list', [GoogleAdsController::class, 'listAssetRelations']); + }); Route::group('/ad', function () { Route::post('/update', [GoogleAdsController::class, 'updateAd']); Route::post('/list', [GoogleAdsController::class, 'listAds']);