From f385f6f2efb6a1c10a5cd353c177e1db506987d3 Mon Sep 17 00:00:00 2001
From: hgc <yellowrna@gmail.com>
Date: Fri, 27 Dec 2024 17:07:27 +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=902?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 app/controller/GoogleAdsController.php        |  39 +
 app/event/GoogleAdsAssetRelations.php         | 297 ++++++++
 app/model/Ad.php                              |   5 +
 app/process/UpdateGoogleAdsTask.php           |  44 +-
 app/service/GoogleAdsAssetRelationService.php | 679 ++++++++++++++++++
 app/service/GoogleAdsAssetService.php         |   6 +-
 config/event.php                              |   7 +
 config/route.php                              |   3 +
 8 files changed, 1062 insertions(+), 18 deletions(-)
 create mode 100644 app/event/GoogleAdsAssetRelations.php
 create mode 100644 app/service/GoogleAdsAssetRelationService.php

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 @@
+<?php
+
+namespace app\event;
+
+use app\service\GoogleAdsAssetRelationService;
+use app\service\GoogleOAuthService;
+use Google\ApiCore\ApiException;
+use GuzzleHttp\Client;
+use GuzzleHttp\Exception\GuzzleException;
+use support\Db;
+use support\Request;
+use support\Response;
+use DI\Annotation\Inject;
+
+//use QL\QueryList;
+use support\Redis;
+
+
+class GoogleAdsAssetRelations
+{
+    /**
+     * @Inject
+     * @var GoogleOAuthService
+     */
+
+    private $googleOAuthService;
+
+
+    //微博热榜地址
+//    const url = 'https://library.tiktok.com/api/v1/search?region=GB&type=1&start_time=1666540800&end_time=1666627200';
+
+    const  userAgent = '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';
+
+    const IMAGEASSET = 'googleadsassetrelations_image';
+    const VIDEOASSET = 'googleadsassetrelations_video';
+    const limit = 12;
+    const sort_order = 'impression,desc';
+
+
+//    const countries = ["AT", "BE", "BG", "HR", "CY", "CZ", "DK", "EE", "FI", "FR", "DE", "GR", "HU", "IS", "IE","IT", "LV", "LI", "LT", "LU", "MT", "NL", "NO", "PL", "PT", "RO", "SK", "SI", "ES", "SE", "CH", "TR", "GB"] ;
+    const countries = ["GB", "BE"];
+
+
+    public function listAssetRelations(Request $request)
+    {
+        $options = $request->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 @@
+<?php
+
+namespace app\service;
+
+use app\service\GoogleAdsClientService;
+use app\model\ThirdUserAdvertiser;
+use app\util\Helper;
+use app\util\ArgumentNames;
+use app\util\ArgumentParser;
+
+//use Google\Ads\GoogleAds\Lib\V18\GoogleAdsClient;
+//use Google\Ads\GoogleAds\Util\FieldMasks;
+//use Google\Ads\GoogleAds\Util\V18\ResourceNames;
+//use Google\Ads\GoogleAds\V18\Common\ResponsiveSearchAdInfo;
+//use Google\Ads\GoogleAds\V18\Enums\AdGroupAdStatusEnum\AdGroupAdStatus;
+//use Google\Ads\GoogleAds\V18\Enums\GoogleAdsFieldCategoryEnum\GoogleAdsFieldCategory;
+//use Google\Ads\GoogleAds\V18\Enums\GoogleAdsFieldDataTypeEnum\GoogleAdsFieldDataType;
+//use Google\Ads\GoogleAds\V18\Errors\GoogleAdsError;
+//use Google\Ads\GoogleAds\V18\Resources\Ad;
+//use Google\Ads\GoogleAds\V18\Resources\AdGroupAd;
+//use Google\Ads\GoogleAds\V18\Resources\GoogleAdsField;
+//use Google\Ads\GoogleAds\V18\Services\AdGroupAdOperation;
+//use Google\Ads\GoogleAds\V18\Services\AdOperation;
+//use Google\Ads\GoogleAds\V18\Services\MutateAdGroupAdsRequest;
+//
+//use Google\Ads\GoogleAds\V18\Common\AdTextAsset;
+//use Google\Ads\GoogleAds\V18\Enums\ServedAssetFieldTypeEnum\ServedAssetFieldType;
+//use Google\Ads\GoogleAds\V18\Services\GoogleAdsRow;
+//use Google\Ads\GoogleAds\V18\Services\MutateAdsRequest;
+//use Google\Ads\GoogleAds\V18\Services\SearchGoogleAdsFieldsRequest;
+//use Google\Ads\GoogleAds\V18\Services\SearchGoogleAdsRequest;
+//use Google\Ads\GoogleAds\V18\Services\SearchGoogleAdsStreamRequest;
+//use Google\Protobuf\Internal\RepeatedField;
+
+
+use think\facade\Db as ThinkDb;
+use app\model\Ad as AdModel;
+use app\model\Asset as AssetModel;
+use app\model\AssetRelation as AssetRelationModel;
+
+
+use Google\ApiCore\ApiException;
+
+class GoogleAdsAssetRelationService extends BaseService
+{
+//    private $googleAdsClient;
+    private $customerId;
+
+    public function __construct($customerId = null)
+    {
+
+    }
+
+    // 从数据库动态获取 google RefreshToken
+//    private function getRefreshTokenFromDatabase($advertiserId)
+//    {
+//        // 通过 advertiser_id 查询 ThirdUserAdvertiser,联表查询 ThirdUser 数据
+//        $userAdvertiser = ThirdUserAdvertiser::with('googleUser')  // 联表查询 user 关联
+//        ->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']);