本節說明如何將庫存實體的時間敏感更新內容傳送給 Google。即時更新 API 可讓您在沙箱或正式版廣告空間中,以近乎即時的方式推送更新及刪除實體。
這項功能主要用於更新無法預測的資訊,例如緊急關店、移除選單項目或更新選單項目的價格,這些資訊必須迅速反映在 Google UI 中。如果變更不需要立即反映,您可以改用批次攝入。即時更新的處理時間最多為五分鐘。
必要條件
如要實作即時更新功能,請先確認下列項目:
- 已啟用 Maps Booking API:
- 在 GCP 中依序前往「API 和服務」>「程式庫」
- 搜尋「Google Maps Booking API」
- 找出 Sandbox 例項 (「Google Maps Booking API (開發人員版)」),然後按一下「啟用」
- 找出「Google Maps Booking API」的正式版例項,然後點選「啟用」
- 系統會為 GCP 專案建立具有編輯者角色的服務帳戶。詳情請參閱「帳戶設定」。
- 主控台或沙箱資料動態饋給會代管及擷取。詳情請參閱「批次攝入」。
- 針對 API 驗證,建議您以所選語言安裝 Google 用戶端程式庫。使用「https://www.googleapis.com/auth/mapsbooking」做為 OAuth 範圍。下方程式碼範例使用這些程式庫。否則,您必須手動處理符記交換,如「使用 OAuth 2.0 存取 Google API」一文所述。
總覽
即時更新 API 支援兩種作業。第一個作業是更新現有實體的 upsert。第二個操作是刪除,用於從商品目錄中移除實體。這兩項作業都會在要求主體中列出的一系列實體上執行。您可以在單一 API 呼叫中更新最多 1,000 個實體。API 會接受所有傳入的要求,並將這些要求排入佇列,以利後續處理。因此,RTU 要求會以非同步方式處理。
即時更新 API 會在沙箱和正式環境中運作。沙箱環境可用於測試 API 要求,而正式環境則可用於更新「訂購端對端」使用者可見的內容。兩個環境的主機名稱:
- 沙箱 -
partnerdev-mapsbooking.googleapis.com
- 正式版 -
mapsbooking.googleapis.com
端點
即時更新 API 會公開兩個端點,用於處理廣告空間更新的傳入要求:
- UPSERT -
/v1alpha/inventory/partners/
PARTNER_ID/feeds/owg.v2/record:batchPush
- 刪除 -
/v1alpha/inventory/partners/
PARTNER_ID/feeds/owg.v2/record:batchDelete
參數 PARTNER_ID 會顯示在「帳戶和使用者」頁面中的「合作夥伴 ID」,如以下螢幕截圖所示。
以上述螢幕截圖為例,假設 PARTNER_ID 的值為 10000001,在沙箱和實際環境中傳送 API 要求的完整網址如下所示。
# Sandbox UPSERT
https://partnerdev-mapsbooking.googleapis.com/v1alpha/inventory/partners/10000001/feeds/owg.v2/record:batchPush
# Sandbox DELETE
https://partnerdev-mapsbooking.googleapis.com/v1alpha/inventory/partners/10000001/feeds/owg.v2/record:batchDelete
# Production UPSERT
https://mapsbooking.googleapis.com/v1alpha/inventory/partners/10000001/feeds/owg.v2/record:batchPush
# Production DELETE
https://mapsbooking.googleapis.com/v1alpha/inventory/partners/10000001/feeds/owg.v2/record:batchDelete
更新實體
如要更新商品目錄中的實體,請使用 UPSERT 端點,並傳送 HTTP POST 要求。每個 POST 要求都必須包含 PARTNER_ID 參數,以及包含廣告空間結構定義中列出的任何實體類型的結構化資料的 JSON 酬載。
Upsert 要求酬載
要求主體是含有記錄清單的 JSON 物件。每筆記錄都對應至要更新的實體。它包含 data_record
欄位,其中實體酬載以 Base64 編碼,而 generation_timestamp
則表示實體更新時間:
{ "records": [ { "data_record":"BASE_64_ENCODED_ENTITY", "generation_timestamp":"UPDATE_TIMESTAMP" } ] }
在上述酬載中,請替換下列項目:
BASE_64_ENCODED_ENTITY:實體的 Base64 編碼 JSON 字串。解碼的實體 JSON 應具有與動態饋給規格相同的結構,例如:
{"@type":"MenuSection","name":"My Updated Menu Section","menuId":{"@id":"10824","displayOrder":1},"@id":"853705"}
UPDATE_TIMESTAMP:請務必附上實體在後端系統中產生的時間戳記。這個時間戳記可用於確保商品目錄更新的正確順序。如果未納入這個欄位,系統會將其設為 Google 收到要求的時間。透過
batchPush
要求更新實體時,generation_timestamp
欄位會用於實體版本管理。請參閱關聯目錄結構定義中的時間值預期格式。
每個即時更新要求都必須符合下列條件:
- 酬載內容不得超過 5 MB。與批次動態饋給類似,建議您為塞入更多資料而移除空格。
batchPush
要求最多可包含 1,000 個實體。
範例
範例 1:更新餐廳
假設你需要緊急更新餐廳的電話號碼,更新內容包含整個餐廳的 JSON。
請參考以下批次動態饋給:
{ "@type": "Restaurant", "@id": "restaurant12345", "name": "Some Restaurant", "url": "https://www.provider.com/somerestaurant", "telephone": "+16501234570", "streetAddress": "345 Spear St", "addressLocality": "San Francisco", "addressRegion": "CA", "postalCode": "94105", "addressCountry": "US", "latitude": 37.472842, "longitude": -122.217144 }
因此,透過 HTTP POST 進行即時更新的程式碼如下:
JSON
POST v1alpha/inventory/partners/PARTNER_ID/feeds/owg.v2/record:batchPush Host: mapsbooking.googleapis.com Content-Type: application/json { "records": [ { "data_record": { "@type": "Restaurant", "@id": "restaurant12345", "name": "Some Restaurant", "url": "https://www.provider.com/somerestaurant", "telephone": "+16501234570", "streetAddress": "345 Spear St", "addressLocality": "San Francisco", "addressRegion": "CA", "postalCode": "94105", "addressCountry": "US", "latitude": 37.472842, "longitude": -122.217144 } "generation_timestamp": "2022-08-19T17:11:10.750Z" } ] }
Base64
同一個範例,但使用 Base64 編碼的酬載。
POST v1alpha/inventory/partners/PARTNER_ID/feeds/owg.v2/record:batchPush Host: mapsbooking.googleapis.com Content-Type: application/json { "records": [ { "data_record": "eyJAdHlwZSI6IlJlc3RhdXJhbnQiLCJAaWQiOiJyZXN0YXVyYW50MTIzNDUiLCJuYW1lIjoiU29tZSBSZXN0YXVyYW50IiwidXJsIjoiaHR0cHM6Ly93d3cucHJvdmlkZXIuY29tL3NvbWVyZXN0YXVyYW50IiwidGVsZXBob25lIjoiKzE2NTAxMjM0NTcwIiwic3RyZWV0QWRkcmVzcyI6IjM0NSBTcGVhciBTdCIsImFkZHJlc3NMb2NhbGl0eSI6IlNhbiBGcmFuY2lzY28iLCJhZGRyZXNzUmVnaW9uIjoiQ0EiLCJwb3N0YWxDb2RlIjoiOTQxMDUiLCJhZGRyZXNzQ291bnRyeSI6IlVTIiwibGF0aXR1ZGUiOjM3LjQ3Mjg0MiwibG9uZ2l0dWRlIjotMTIyLjIxNzE0NH0=" "generation_timestamp": "2022-08-19T17:11:10.750Z" } ] }
範例 2:更新多間餐廳
如要在單一 API 呼叫中更新兩個餐廳實體,HTTP POST 要求會如下所示:
JSON
POST v1alpha/inventory/partners/PARTNER_ID/feeds/owg.v2/record:batchPush Host: mapsbooking.googleapis.com Content-Type: application/json { "records": [ { "data_record": { "@type": "Restaurant", "@id": "restaurant12345", "name": "Some Restaurant", "url": "https://www.provider.com/somerestaurant", "telephone": "+16501235555", "streetAddress": "345 Spear St", "addressLocality": "San Francisco", "addressRegion": "CA", "postalCode": "94105", "addressCountry": "US", "latitude": 37.472842, "longitude": -122.217144 }, "generation_timestamp": "2022-08-19T17:11:10.850Z" }, { "data_record": { "@type": "Restaurant", "@id": "restaurant123", "name": "Some Other Restaurant", "url": "https://www.provider.com/someotherrestaurant", "telephone": "+16501231235", "streetAddress": "385 Spear St", "addressLocality": "San Mateo", "addressRegion": "CA", "postalCode": "94115", "addressCountry": "US" }, "generation_timestamp": "2022-08-19T17:11:10.850Z" } ] }
Base64
同一個範例,但使用 Base64 編碼的酬載。
POST v1alpha/inventory/partners/PARTNER_ID/feeds/owg.v2/record:batchPush Host: mapsbooking.googleapis.com Content-Type: application/json { "records": [ { "data_record": "eyJAdHlwZSI6IlJlc3RhdXJhbnQiLCJAaWQiOiJyZXN0YXVyYW50MTIzNDUiLCJuYW1lIjoiU29tZSBSZXN0YXVyYW50IiwidXJsIjoiaHR0cHM6Ly93d3cucHJvdmlkZXIuY29tL3NvbWVyZXN0YXVyYW50IiwidGVsZXBob25lIjoiKzE2NTAxMjM1NTU1Iiwic3RyZWV0QWRkcmVzcyI6IjM0NSBTcGVhciBTdCIsImFkZHJlc3NMb2NhbGl0eSI6IlNhbiBGcmFuY2lzY28iLCJhZGRyZXNzUmVnaW9uIjoiQ0EiLCJwb3N0YWxDb2RlIjoiOTQxMDUiLCJhZGRyZXNzQ291bnRyeSI6IlVTIiwibGF0aXR1ZGUiOjM3LjQ3Mjg0MiwibG9uZ2l0dWRlIjotMTIyLjIxNzE0NH0=", "generation_timestamp": "2022-08-19T17:11:10.850Z" }, { "data_record": "eyJAdHlwZSI6IlJlc3RhdXJhbnQiLCJAaWQiOiJyZXN0YXVyYW50MTIzIiwibmFtZSI6IlNvbWUgT3RoZXIgUmVzdGF1cmFudCIsInVybCI6Imh0dHBzOi8vd3d3LnByb3ZpZGVyLmNvbS9zb21lcmVzdGF1cmFudCIsInRlbGVwaG9uZSI6IisxNjUwMTIzMTIzNSIsInN0cmVldEFkZHJlc3MiOiIzODUgU3BlYXIgU3QiLCJhZGRyZXNzTG9jYWxpdHkiOiJTYW4gTWF0ZW8iLCJhZGRyZXNzUmVnaW9uIjoiQ0EiLCJwb3N0YWxDb2RlIjoiOTQxMTUiLCJhZGRyZXNzQ291bnRyeSI6IlVTIn0=", "generation_timestamp": "2022-08-19T17:11:10.850Z" } ] }
示例 3:更新菜單價格
假設您需要變更菜單項目的價格。
請參考以下批次動態饋給:
{ "@type": "MenuItemOffer", "@id": "menuitemoffer6680262", "sku": "offer-cola", "menuItemId": "menuitem896532", "price": 2, "priceCurrency": "USD" }
透過 POST 進行即時更新的程式碼如下:
JSON
POST v1alpha/inventory/partners/PARTNER_ID/feeds/owg.v2/record:batchPush Host: mapsbooking.googleapis.com Content-Type: application/json { "records": [ { "data_record": { "@type": "MenuItemOffer", "@id": "menuitemoffer6680262", "sku": "offer-cola", "menuItemId": "menuitem896532", "price": 2, "priceCurrency": "USD" }, "generation_timestamp": "2022-08-19T17:20:10Z" } ] }
Base64
同一個範例,但使用 Base64 編碼的酬載。
POST v1alpha/inventory/partners/PARTNER_ID/feeds/owg.v2/record:batchPush Host: mapsbooking.googleapis.com Content-Type: application/json { "records": [ { "data_record": "eyJAdHlwZSI6Ik1lbnVJdGVtT2ZmZXIiLCJAaWQiOiJtZW51aXRlbW9mZmVyNjY4MDI2MiIsInNrdSI6Im9mZmVyLWNvbGEiLCJtZW51SXRlbUlkIjoibWVudWl0ZW04OTY1MzIiLCJwcmljZSI6MiwicHJpY2VDdXJyZW5jeSI6IlVTRCJ9", "generation_timestamp": "2022-08-19T17:20:10Z" } ] }
新增實體
請勿使用即時更新功能新增實體,否則可能會導致資料不一致。請改用批次動態饋給程序,如批次攝入一節所述。
刪除實體
如要從廣告空間中刪除實體,請使用 DELETE 端點,並傳送 HTTP POST 要求。每個 POST 要求都必須包含 PARTNER_ID 參數,以及含有目錄中任何實體的 ID 的 JSON 酬載。
刪除要求酬載
刪除要求的主體結構與更新要求類似。它也包含含有 data_record
和 delete_time
欄位的記錄清單:
{ "records": [ { "data_record":"BASE_64_ENCODED_REFERENCE", "delete_time": "DELETE_TIMESTAMP" } ] }
在上述酬載中,請替換下列項目:
BASE_64_ENCODED_REFERENCE:所要移除實體參照的 Base64 編碼 JSON 字串。參照項目只包含實體類型和 ID,例如 MenuSection 參照項目的 JSON 表示法:
{"@type":"MenuSection","@id":"853705"}
DELETE_TIMESTAMP:請務必附上實體在後端系統中刪除的時間戳記。這個時間戳記會用來決定刪除作業在廣告空間中執行的順序。
batchDelete
要求最多可包含 1,000 個實體。
範例
範例 1:移除兩個 MenuItem
實體
如要在單一 API 呼叫中移除兩個選單項目,HTTP POST 要求會如下所示:
JSON
POST v1alpha/inventory/partners/PARTNER_ID/feeds/owg.v2/record:batchDelete Host: mapsbooking.googleapis.com Content-Type: application/json { "records": [ { "data_record": { "@type": "MenuItem", "@id": "item_1234" }, "delete_time": "2022-08-21T15:23:00.000Z" }, { "data_record": { "@type": "MenuItem", "@id": "item_5678" }, "delete_time": "2022-08-21T15:23:00.000Z" } ] }
Base64
同一個範例,但使用 Base64 編碼的酬載。
POST v1alpha/inventory/partners/PARTNER_ID/feeds/owg.v2/record:batchDelete Host: mapsbooking.googleapis.com Content-Type: application/json { "records": [ { "data_record": "eyJAdHlwZSI6Ik1lbnVJdGVtIiwiQGlkIjoiaXRlbV8xMjM0In0=" "delete_time": "2022-08-21T15:23:00.000Z" }, { "data_record": "eyJAdHlwZSI6Ik1lbnVJdGVtIiwiQGlkIjoiaXRlbV81Njc4In0=" "delete_time": "2022-08-21T15:23:00.000Z" }, ] }
範例 2:刪除 Restaurant
實體
假設你想刪除批次動態饋給中的餐廳,請務必只刪除餐廳實體。請勿刪除服務和選單等子實體,因為系統會自動移除這些項目。
刪除 ID 為 https://www.provider.com/restaurant/12345
的餐廳實體的示例要求:
JSON
POST v1alpha/inventory/partners/PARTNER_ID/feeds/owg.v2/record:batchDelete Host: mapsbooking.googleapis.com Content-Type: application/json { "records": [ { "data_record": { "@type": "Restaurant", "@id": "https://www.provider.com/restaurant/12345" }, "delete_time": "2022-08-19T17:11:10.750Z" } ] }
Base64
同一個範例,但使用 Base64 編碼的酬載。
POST v1alpha/inventory/partners/PARTNER_ID/feeds/owg.v2/record:batchDelete Host: mapsbooking.googleapis.com Content-Type: application/json { "records": [ { "data_record": "ewogICJAdHlwZSI6ICJSZXN0YXVyYW50IiwKICAiQGlkIjogImh0dHBzOi8vd3d3LnByb3ZpZGVyLmNvbS9yZXN0YXVyYW50LzEyMzQ1Igp9" "delete_time": "2022-08-19T17:11:10.750Z" } ] }
驗證與 API 回應代碼
系統會對即時更新 API 呼叫執行兩種驗證:
要求層級:這些驗證會檢查酬載是否遵循更新/插入或刪除結構定義,且每個
data_record
都包含@id
和@type
欄位。這些檢查是同步的,且結果會傳回至 API 回應主體。回應碼 200 和空白的 JSON 主體{}
表示這些驗證已通過,且該要求中的實體已排入處理佇列。回應代碼與 200 不同,表示一或多項驗證失敗,且整個要求遭到拒絕 (包括酬載中的所有實體)。舉例來說,如果data_record
缺少@type
,系統會傳回以下錯誤回應:{ "error": { "code": 400, "message": "Record:{\"@id\":\"2717/86853/DELIVERY\",\"applicableServiceType\":[\"DELIVERY\",\"TAKEOUT\"],\"menuId\":[{\"@id\":\"2717/DELIVERY\",\"displayOrder\":1},{\"@id\":\"2717/TAKEOUT\",\"displayOrder\":2}],\"name\":\"Salad\",\"offeredById\":[\"2717\"]} has following errors: \nThe entity type could not be extracted from the entity value.\n", "status": "INVALID_ARGUMENT", "details": [ { "@type": "type.googleapis.com/google.rpc.DebugInfo", "detail": "[ORIGINAL ERROR] generic::invalid_argument: Failed to parse one or more rtu records. Record:{\"@id\":\"2717/86853/DELIVERY\",\"applicableServiceType\":[\"DELIVERY\",\"TAKEOUT\"],\"menuId\":[{\"@id\":\"2717/DELIVERY\",\"displayOrder\":1},{\"@id\":\"2717/TAKEOUT\",\"displayOrder\":2}],\"name\":\"Salad\",\"offeredById\":[\"2717\"]} has following errors: \nThe entity type could not be extracted from the entity value.\n [google.rpc.error_details_ext] { message: \"Record:{\\\"@id\\\":\\\"2717/86853/DELIVERY\\\",\\\"applicableServiceType\\\":[\\\"DELIVERY\\\",\\\"TAKEOUT\\\"],\\\"menuId\\\":[{\\\"@id\\\":\\\"2717/DELIVERY\\\",\\\"displayOrder\\\":1},{\\\"@id\\\":\\\"2717/TAKEOUT\\\",\\\"displayOrder\\\":2}],\\\"name\\\":\\\"Salad\\\",\\\"offeredById\\\":[\\\"2717\\\"]} has following errors: \\nThe entity type could not be extracted from the entity value.\\n\" }" } ] } }
實體層級:系統會根據關聯式結構定義驗證酬載中的每個實體。在這個驗證階段遇到的問題不會在 API 回應中回報。這些資料只會在 RTU 報表資訊主頁中回報。
API 配額
即時 API 更新的配額為每 60 秒 1,500 個要求,或平均每秒 25 個要求。超過配額上限時,Google 會回應下列錯誤訊息:
{ "error": { "code": 429, "message": "Insufficient tokens for quota ...", "status": "RESOURCE_EXHAUSTED", "details": [...] } }
如要處理這個問題,請按指數增長的時距 重新嘗試呼叫,直到呼叫成功。如果您經常用完配額,請考慮在單一 API 要求中加入更多實體。您最多可以在一個 API 呼叫中加入 1,000 個實體。
程式碼範例
以下列舉一些範例,說明如何使用各種語言的即時更新 API。這些範例會使用 Google 驗證程式庫,透過帳戶設定期間產生的服務帳戶金鑰檔案進行驗證。如需其他解決方案,請參閱「針對伺服器對伺服器應用程式使用 OAuth 2.0」一文。建議您使用「產生用戶端程式庫」中的結構定義,為商品目錄和即時更新物件類型產生原始碼。
更新實體
Node.js
本程式碼使用 Node.js 適用的 Google 驗證程式庫。
/* Sample code for Real-time update batchPush implementation. * * Required libraries: * - google-auth-library */ const {JWT} = require('google-auth-library'); // ACTION REQUIRED: Change this to the path of the service account client secret // file downloaded from the Google Cloud Console. const serviceAccountJson = require('./service-account.json'); // ACTION REQUIRED: Change this to your Partner ID received from Google. // The Partner ID is available on the Partner Portal. const PARTNER_ID = 1234; const HOST = { prod: 'https://mapsbooking.googleapis.com', sandbox: 'https://partnerdev-mapsbooking.googleapis.com' }; // ACTION REQUIRED: Change to 'prod' for production const ENV = 'sandbox'; // Feed name for Order with Google including the version. const FEED_NAME = 'owg.v2'; // Endpoint url const url = `${HOST[ENV]}/v1alpha/inventory/partners/${PARTNER_ID}/feeds/${ FEED_NAME}/record:batchPush`; /** * Send a Real-time update request to update/insert entities */ async function batchUpsert(entities) { /** * Sign JWT token using private key from service account secret file * provided. The client can be created without providing a service account * secret file by implementing Application Default Credentials. * https://github.com/googleapis/google-auth-library-nodejs */ const client = new JWT({ email: serviceAccountJson.client_email, key: serviceAccountJson.private_key, scopes: ['https://www.googleapis.com/auth/mapsbooking'], }); const request = {records: toPushRecords(entities)}; const body = JSON.stringify(request); try { const response = await client.request({ method: 'POST', url, data: body, headers: {'Content-Type': 'application/json'} }); console.log('request body:', body); console.log('response status:', response.status); console.log( 'response data:', response.data); // successful response returns '{}' } catch (error) { console.log('error:', error); } } /** * Maps array of entities to records for batch push requests */ const toPushRecords = (entities) => { return entities.map((entity) => { // Using dateModified to set generation_timestamp. Defaulting to the // current timestamp for records that do not have dateModified. const generation_timestamp = entity.dateModified ? entity.dateModified : new Date().toISOString(); return {data_record: btoa(JSON.stringify(entity)), generation_timestamp}; }); }; // Call batchUpsert with example entities. dateModified is optional and is // used to hold the actual timestamp when the entity was updated/created. batchUpsert([ { '@type': 'MenuItemOffer', '@id': '6680261', 'menuItemId': '18931508', 'price': 15.5, 'priceCurrency': 'USD', 'applicableServiceType': ['DELIVERY', 'TAKEOUT'], 'inventoryLevel': 0, 'dateModified': '2022-06-19T15:43:50.970Z' }, { '@type': 'MenuItemOffer', '@id': '6680262', 'menuItemId': '18931509', 'price': 25.5, 'priceCurrency': 'USD', 'applicableServiceType': ['DELIVERY', 'TAKEOUT'], 'inventoryLevel': 0, 'dateModified': '2022-06-19T15:43:50.970Z' } ]);
Python
這個程式碼使用 Python 專用的 Google 驗證程式庫。
"""Sample code for the Real-time update batchPush implementation.""" # Required libraries: # - google-auth import base64 import datetime import json from google.auth.transport.requests import AuthorizedSession from google.oauth2 import service_account # ACTION REQUIRED: Change this to the Partner ID received from Google. # Partner ID is available on the Partner Portal. # https://partnerdash.google.com/apps/reservewithgoogle _PARTNER_ID = '1234' # ACTION REQUIRED: Change this to the path of the service account client secret # file downloaded from the Google Cloud Console. _SERVICE_ACCOUNT_KEY_JSON_FILE = 'service-account-creds.json' _HOST_MAP = { 'sandbox': 'https://partnerdev-mapsbooking.googleapis.com', 'prod': 'https://mapsbooking.googleapis.com' } # ACTION REQUIRED: Change to 'prod' for production _ENV = 'sandbox' # Feed name for Order with Google including the version. _FEED_NAME = 'owg.v2' _ENDPOINT = '{}/v1alpha/inventory/partners/{}/feeds/{}/record:batchPush'.format( _HOST_MAP[_ENV], _PARTNER_ID, _FEED_NAME) def batch_upsert(entities): """Makes a batchPush request using the Real-time updates REST service. Args: entities: The list of entity objects to update or add. """ # Creates credentials by providing a json file. Credentials can also be # provided by implementing Application Default Credentials. # https://googleapis.dev/python/google-auth/latest/user-guide.html credentials = service_account.Credentials.from_service_account_file( _SERVICE_ACCOUNT_KEY_JSON_FILE, scopes=['https://www.googleapis.com/auth/mapsbooking']) authorized_session = AuthorizedSession(credentials) # JSON request object batch_request = {'records': [create_push_record(x) for x in entities]} response = authorized_session.post(_ENDPOINT, json=batch_request) print('request body:', json.dumps(batch_request)) print('response status:', response.status_code) print('response data:', response.text) # successful response returns '{}' def create_push_record(entity): """Creates a record from an entity for batchPush requests. Args: entity: The entity object to create the record from. Returns: The constructed record for the batchPush request payload. """ data_bytes = json.dumps(entity).encode('utf-8') base64_bytes = base64.b64encode(data_bytes) # Using dateModified to set generation_timestamp. Defaulting to the # current timestamp for records that do not have dateModified. generation_timestamp = entity.dateModified if 'dateModified' in entity else datetime.datetime.now( ).strftime('%Y-%m-%dT%H:%M:%S.%fZ') return { 'generation_timestamp': generation_timestamp, 'data_record': base64_bytes.decode('utf-8') } # Call batch_upsert with example entities. dateModified is optional and is # used to hold the actual timestamp when the entity was updated/created. batch_upsert([{ '@type': 'MenuItemOffer', '@id': '6680261', 'menuItemId': '18931508', 'price': 15.5, 'priceCurrency': 'USD', 'applicableServiceType': ['DELIVERY', 'TAKEOUT'], 'inventoryLevel': 0, 'dateModified': '2022-06-19T15:43:50.970Z' }, { '@type': 'MenuItemOffer', '@id': '6680262', 'menuItemId': '18931509', 'price': 25.5, 'priceCurrency': 'USD', 'applicableServiceType': ['DELIVERY', 'TAKEOUT'], 'inventoryLevel': 0, 'dateModified': '2022-06-19T15:43:50.970Z' }])
Java
此程式碼使用 Java 專用的 Google 授權程式庫。
套件 rtusamples.inventory
和 rtusamples.realtime
中的用戶端原始碼模型,是按照「產生用戶端程式庫」一節中的步驟建立。
/* * Required Libraries: * - JDK >= 11 * - google-auth-library-oauth2-http */ package rtusamples; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.auth.oauth2.AccessToken; import com.google.auth.oauth2.GoogleCredentials; import java.io.FileInputStream; import java.io.IOException; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpRequest.BodyPublishers; import java.net.http.HttpResponse; import java.net.http.HttpResponse.BodyHandlers; import java.nio.charset.Charset; import java.time.Clock; import java.time.OffsetDateTime; import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.Arrays; import java.util.Base64; import java.util.List; import java.util.Optional; import java.util.stream.Collectors; import rtusamples.inventory.MenuItemOffer; import rtusamples.inventory.MenuItemOfferType; import rtusamples.inventory.ServiceTypeElement; import rtusamples.realtime.BatchPushGenericRecordRequest; import rtusamples.realtime.GenericRecord; /** Sample code for Real-time update batchPush implementation. */ public final class BasicPush { // ACTION REQUIRED: Change this to your Partner ID received from Google. The Partner ID is // available on the Partner Portal. private static final long PARTNER_ID = 12345678; // ACTION REQUIRED: Change this to the path of the service account client secret file downloaded // from the Google Cloud Console. private static final String JSON_KEY_FULL_PATH = "<path to your JSON credentials>/credentials.json"; // ACTION REQUIRED: Change this to the endpoint that is needed. private static final String ENDPOINT = // "https://partnerdev-mapsbooking.googleapis.com"; // for sandbox "https://mapsbooking.googleapis.com"; // for prod // Feed name for Order with Google including the version. private static final String FEED_NAME = "owg.v2"; private static final ObjectMapper objectMapper = new ObjectMapper(); private static final DateTimeFormatter TIMESTAMP_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss[.SSS]'Z'"); private static final Charset UTF_8 = Charset.forName("UTF-8"); public static void main(String[] args) throws Exception { /** * Create credentials from service account secret file. Alternatively, the credentials can be * created by implementing Application Default Credentials. * https://github.com/googleapis/google-auth-library-java */ // GoogleCredentials sourceCredentials = // GoogleCredentials.getApplicationDefault() // .createScoped(Arrays.asList("https://www.googleapis.com/auth/mapsbooking")); // ImpersonatedCredentials credentials = // ImpersonatedCredentials.create( // sourceCredentials, // "fo-test@projectname.iam.gserviceaccount.com", // null, // Arrays.asList("https://www.googleapis.com/auth/mapsbooking"), // 300); GoogleCredentials credentials = GoogleCredentials.fromStream(new FileInputStream(JSON_KEY_FULL_PATH)) .createScoped(Arrays.asList("https://www.googleapis.com/auth/mapsbooking")); // Create example MenuItemOffer entities, dateModified is optional and is used to hold // the actual timestamp when the entity was updated/created. MenuItemOffer menuItemOfferPizza = new MenuItemOffer(); menuItemOfferPizza.setID("6680261"); menuItemOfferPizza.setType(MenuItemOfferType.MENU_ITEM_OFFER); menuItemOfferPizza.setMenuItemID("18931508"); menuItemOfferPizza.setPrice(15.5); menuItemOfferPizza.setPriceCurrency("USD"); menuItemOfferPizza.setApplicableServiceType( new ServiceTypeElement[] {ServiceTypeElement.TAKEOUT, ServiceTypeElement.DELIVERY}); menuItemOfferPizza.setInventoryLevel(0.0); menuItemOfferPizza.setDateModified("2022-10-07T13:00:00.000Z"); MenuItemOffer menuItemOfferSalad = new MenuItemOffer(); menuItemOfferSalad.setID("6680262"); menuItemOfferSalad.setType(MenuItemOfferType.MENU_ITEM_OFFER); menuItemOfferSalad.setMenuItemID("18931509"); menuItemOfferSalad.setPrice(25.5); menuItemOfferSalad.setPriceCurrency("USD"); menuItemOfferSalad.setApplicableServiceType( new ServiceTypeElement[] {ServiceTypeElement.TAKEOUT, ServiceTypeElement.DELIVERY}); menuItemOfferSalad.setInventoryLevel(0.0); menuItemOfferSalad.setDateModified("2022-10-07T13:00:00.000Z"); // Example array of MenuItemOffer entities to update. List<MenuItemOffer> menuItemOffers = Arrays.asList(menuItemOfferPizza, menuItemOfferSalad); // Create list of GenericRecord from menuItemOffers. List<GenericRecord> menuItemOfferGenericRecords = menuItemOffers.stream() .map( (menuItemOffer) -> toBatchPushRecord(menuItemOffer, menuItemOffer.getDateModified())) .collect(Collectors.toList()); // List of records to be updated/created. List<GenericRecord> recordsToBeUpdated = new ArrayList<>(); // Add list of menuItemOffer generic records. recordsToBeUpdated.addAll(menuItemOfferGenericRecords); // Request object that contains all records. BatchPushGenericRecordRequest batchPushRequest = new BatchPushGenericRecordRequest(); batchPushRequest.setRecords(recordsToBeUpdated.toArray(new GenericRecord[0])); // Execute batchPush request. BasicPush basicPush = new BasicPush(); basicPush.batchPush(batchPushRequest, credentials); } public void batchPush( BatchPushGenericRecordRequest batchPushRequest, GoogleCredentials credentials) throws IOException { credentials.refreshIfExpired(); AccessToken token = credentials.getAccessToken(); String requestBody = objectMapper.writeValueAsString(batchPushRequest); HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri( URI.create( String.format( "%s/v1alpha/inventory/partners/%s/feeds/%s/record:batchPush", ENDPOINT, PARTNER_ID, FEED_NAME))) .header("Content-Type", "application/json") .header("Authorization", String.format("Bearer %s", token.getTokenValue())) .POST(BodyPublishers.ofString(requestBody)) .build(); HttpResponse<String> response = null; try { response = client.send(request, BodyHandlers.ofString()); System.out.println("Request body:" + requestBody); System.out.println("Response status:" + response.statusCode()); System.out.println("Response body:" + response.body()); } catch (IOException | InterruptedException e) { e.printStackTrace(); } } public static <T> GenericRecord toBatchPushRecord(T entity, String dateModified) { GenericRecord genericRecord = new GenericRecord(); try { String json = objectMapper.writeValueAsString(entity); genericRecord.setDataRecord(Base64.getEncoder().encodeToString(json.getBytes(UTF_8))); // Using dateModified to set generation_timestamp. Defaulting to the // current timestamp for records that do not have dateModified. String generationTimestamp = Optional.ofNullable(dateModified) .orElse(OffsetDateTime.now(Clock.systemUTC()).format(TIMESTAMP_FORMATTER)); genericRecord.setGenerationTimestamp(generationTimestamp); } catch (JsonProcessingException e) { System.out.println(e.getMessage()); } return genericRecord; } }
移除實體
Node.js
本程式碼使用 Node.js 適用的 Google 驗證程式庫。
/* Sample code for Real-time update batchDelete implementation. * * Required libraries: * - google-auth-library */ const {JWT} = require('google-auth-library'); // ACTION REQUIRED: Change this to the path of the service account client secret // file downloaded from the Google Cloud Console. const serviceAccountJson = require('./service-account.json'); // ACTION REQUIRED: Change this to your Partner ID received from Google. // The Partner ID is available on the Partner Portal. const PARTNER_ID = 1234; const HOST = { prod: 'https://mapsbooking.googleapis.com', sandbox: 'https://partnerdev-mapsbooking.googleapis.com' }; // ACTION REQUIRED: Change to 'prod' for production const ENV = 'sandbox'; // Feed name for Order with Google including the version. const FEED_NAME = 'owg.v2'; // Endpoint url const url = `${HOST[ENV]}/v1alpha/inventory/partners/${PARTNER_ID}/feeds/${ FEED_NAME}/record:batchDelete`; /** * Send a Real-time update request to delete entities */ async function batchDelete(entities) { try { /** * Sign JWT token using private key from service account secret file * provided. The client can be created without providing a service account * secret file by implementing Application Default Credentials. * https://github.com/googleapis/google-auth-library-nodejs */ const client = new JWT({ email: serviceAccountJson.client_email, key: serviceAccountJson.private_key, scopes: ['https://www.googleapis.com/auth/mapsbooking'], }); const request = { records: toDeleteRecords(entities) }; const body = JSON.stringify(request); try { const response = await client.request({ method: 'POST', url, data: body, headers: {'Content-Type': 'application/json'} }); console.log('request body:', body); console.log('response status:', response.status); console.log('response data:', response.data); // successful response returns '{}' } catch (error) { console.log('error:', error); } } /** * Maps array of entities to records for batch delete requests */ const toDeleteRecords = (entities) => { return entities.map((entity) => { // Using dateModified to set delete_time. Defaulting to the current // timestamp for records that do not have dateModified. const delete_time = entity.dateModified ? entity.dateModified : new Date().toISOString(); return {data_record: btoa(JSON.stringify(entity)), delete_time}; }); }; // Call batchDelete with example entities. dateModified is optional and is // used to hold the actual timestamp when the entity was deleted. batchDelete([ { '@type': 'Menu', '@id': '853706', 'dateModified': '2022-06-19T15:43:50.970Z' }, { '@type': 'Menu', '@id': '853705', 'dateModified': '2022-06-19T15:13:00.280Z' } ]);
Python
這個程式碼使用 Python 專用的 Google 驗證程式庫。
"""Sample code for the Real-time update batchDelete implementation.""" # Required libraries: # - google-auth import base64 import datetime import json from google.auth.transport.requests import AuthorizedSession from google.oauth2 import service_account # ACTION REQUIRED: Change this to the Partner ID received from Google. # Partner ID is available on the Partner Portal. # https://partnerdash.google.com/apps/reservewithgoogle _PARTNER_ID = '1234' # ACTION REQUIRED: Change this to the path of the service account client secret # file downloaded from the Google Cloud Console. _SERVICE_ACCOUNT_KEY_JSON_FILE = 'service-account-creds.json' _HOST_MAP = { 'sandbox': 'https://partnerdev-mapsbooking.googleapis.com', 'prod': 'https://mapsbooking.googleapis.com' } # ACTION REQUIRED: Change to 'prod' for production _ENV = 'sandbox' # Feed name for Order with Google including the version. _FEED_NAME = 'owg.v2' _ENDPOINT = '{}/v1alpha/inventory/partners/{}/feeds/{}/record:batchDelete'.format( _HOST_MAP[_ENV], _PARTNER_ID, _FEED_NAME) def batch_delete(entities): """Makes a batch delete request using the Real-time updates REST service. Args: entities: The list of entity objects to delete. """ # Creates credentials by providing a json file. Credentials can also be # provided by implementing Application Default Credentials. # https://googleapis.dev/python/google-auth/latest/user-guide.html credentials = service_account.Credentials.from_service_account_file( _SERVICE_ACCOUNT_KEY_JSON_FILE, scopes=['https://www.googleapis.com/auth/mapsbooking']) authorized_session = AuthorizedSession(credentials) # JSON request object batch_request = {'records': [create_delete_record(x) for x in entities]} response = authorized_session.post(_ENDPOINT, json=batch_request) print('request body:', json.dumps(batch_request)) print('response status:', response.status_code) print('response data:', response.text) # successful response returns '{}' def create_delete_record(entity): """Creates a record from an entity for batchDelete requests. Args: entity: The entity object to create the record from. Returns: The constructed record for the batchDelete request payload. """ data_bytes = json.dumps(entity).encode('utf-8') base64_bytes = base64.b64encode(data_bytes) # Using dateModified to set delete_time. Defaulting to the current # timestamp for records that do not have dateModified. delete_time = entity.dateModified if 'dateModified' in entity else datetime.datetime.now( ).strftime('%Y-%m-%dT%H:%M:%S.%fZ') return { 'delete_time': delete_time, 'data_record': base64_bytes.decode('utf-8') } # Call batch_delete with example entities. dateModified is optional and is # used to hold the actual timestamp when the entity was deleted. batch_delete([{ '@type': 'Menu', '@id': '853706', 'dateModified': '2022-06-19T13:10:00.000Z' }, { '@type': 'Menu', '@id': '853705', 'dateModified': '2022-06-19T13:30:10.000Z' }])
Java
此程式碼使用 Java 專用的 Google 授權程式庫。
套件 rtusamples.inventory
和 rtusamples.realtime
中的用戶端原始碼模型,是按照「產生用戶端程式庫」一節中的步驟建立。
/* * Required Libraries: * - JDK >= 11 * - google-auth-library-oauth2-http */ package rtusamples; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.auth.oauth2.AccessToken; import com.google.auth.oauth2.GoogleCredentials; import java.io.FileInputStream; import java.io.IOException; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpRequest.BodyPublishers; import java.net.http.HttpResponse; import java.net.http.HttpResponse.BodyHandlers; import java.nio.charset.Charset; import java.time.Clock; import java.time.OffsetDateTime; import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.Arrays; import java.util.Base64; import java.util.List; import java.util.Optional; import java.util.stream.Collectors; import rtusamples.inventory.Menu; import rtusamples.inventory.MenuType; import rtusamples.realtime.BatchDeleteGenericRecordsRequest; import rtusamples.realtime.GenericDeleteRecord; /** Sample code for the Real-time update batchDelete implementation. */ public final class BasicDelete { // ACTION REQUIRED: Change this to your Partner ID received from Google. The Partner ID is // available on the Partner Portal. private static final long PARTNER_ID = 123456789; // ACTION REQUIRED: Change this to the path of the service account client secret file downloaded // from the Google Cloud Console. private static final String JSON_KEY_FULL_PATH = "<path to your JSON credentials>/credentials.json"; // ACTION REQUIRED: Change this to the endpoint that is needed. private static final String ENDPOINT = "https://partnerdev-mapsbooking.googleapis.com"; // for sandbox // "https://mapsbooking.googleapis.com" // for prod // Feed name for Order with Google including the version. private static final String FEED_NAME = "owg.v2"; private static final ObjectMapper objectMapper = new ObjectMapper(); private static final DateTimeFormatter TIMESTAMP_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss[.SSS]'Z'"); private static final Charset UTF_8 = Charset.forName("UTF-8"); public static void main(String[] args) throws Exception { /** * Create credentials from service account secret file. Alternatively, the credentials can be * created by implementing Application Default Credentials. * https://github.com/googleapis/google-auth-library-java */ // GoogleCredentials sourceCredentials = // GoogleCredentials.getApplicationDefault() // .createScoped(Arrays.asList("https://www.googleapis.com/auth/mapsbooking")); // ImpersonatedCredentials credentials = // ImpersonatedCredentials.create( // sourceCredentials, // "fo-test@projectname.iam.gserviceaccount.com", // null, // Arrays.asList("https://www.googleapis.com/auth/mapsbooking"), // 300); GoogleCredentials credentials = GoogleCredentials.fromStream(new FileInputStream(JSON_KEY_FULL_PATH)) .createScoped(Arrays.asList("https://www.googleapis.com/auth/mapsbooking")); // Create example Menu entities, dateModified is optional and is used to hold // the actual timestamp when the entity was deleted. Menu menuLunch = new Menu(); menuLunch.setID("853705"); menuLunch.setType(MenuType.MENU); menuLunch.setDateModified("2022-09-19T13:10:00.000Z"); Menu menuDinner = new Menu(); menuDinner.setID("853706"); menuDinner.setType(MenuType.MENU); menuDinner.setDateModified("2022-09-19T13:13:10.000Z"); // Example array of Menu entities to update. List<Menu> menus = Arrays.asList(menuLunch, menuDinner); // Create list of GenericDeleteRecord from menus. List<GenericDeleteRecord> menuGenericDeleteRecords = menus.stream() .map((menu) -> toBatchDeleteRecord(menu, menu.getDateModified())) .collect(Collectors.toList()); // List of records to be deleted. List<GenericDeleteRecord> recordsToBeDeleted = new ArrayList<>(); // Add list of menu generic records. recordsToBeDeleted.addAll(menuGenericDeleteRecords); // Request object that contains all records. BatchDeleteGenericRecordsRequest batchDeleteRequest = new BatchDeleteGenericRecordsRequest(); batchDeleteRequest.setRecords(recordsToBeDeleted.toArray(new GenericDeleteRecord[0])); // Execute batchDelete request. BasicDelete basicDelete = new BasicDelete(); basicDelete.batchDelete(batchDeleteRequest, credentials); } public void batchDelete( BatchDeleteGenericRecordsRequest batchDeleteRequest, GoogleCredentials credentials) throws IOException { credentials.refreshIfExpired(); AccessToken token = credentials.getAccessToken(); String requestBody = objectMapper.writeValueAsString(batchDeleteRequest); HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri( URI.create( String.format( "%s/v1alpha/inventory/partners/%s/feeds/%s/record:batchDelete", ENDPOINT, PARTNER_ID, FEED_NAME))) .header("Content-Type", "application/json") .header("Authorization", String.format("Bearer %s", token.getTokenValue())) .POST(BodyPublishers.ofString(requestBody)) .build(); HttpResponse<String> response = null; try { response = client.send(request, BodyHandlers.ofString()); System.out.println("Request body:" + requestBody); System.out.println("Response status:" + response.statusCode()); System.out.println("Response body:" + response.body()); } catch (IOException | InterruptedException e) { e.printStackTrace(); } } public static <T> GenericDeleteRecord toBatchDeleteRecord(T entity, String dateModified) { GenericDeleteRecord genericRecord = new GenericDeleteRecord(); try { String json = objectMapper.writeValueAsString(entity); genericRecord.setDataRecord(Base64.getEncoder().encodeToString(json.getBytes(UTF_8))); // Using dateModified to set delete_time. Defaulting to the current // timestamp for records that do not have dateModified. String deleteTime = Optional.ofNullable(dateModified) .orElse(OffsetDateTime.now(Clock.systemUTC()).format(TIMESTAMP_FORMATTER)); genericRecord.setDeleteTime(deleteTime); } catch (JsonProcessingException e) { System.out.println(e.getMessage()); } return genericRecord; } }
用途
以下是即時更新、批次動態饋給更新,以及 API 呼叫中高層級內容的使用案例:
情境 | 要更新的實體 | 說明和影響 |
---|---|---|
停用服務 | Service |
您需要基於不可預測的原因停用服務。 即時更新:更新問題中的 完整動態饋給:請務必在 Google 下次擷取前,從完整動態饋給更新實體,將 |
特定商品缺貨 | MenuItemOffer |
即時更新:傳送封裝的 MenuItemOffer 實體,其中 inventoryLevel 已針對指定的 MenuItem 設為 0,且所有其他資料皆保持不變。 |
菜單項目價格異動 | MenuItemOffer |
即時更新:傳送封裝的 MenuItemOffer 實體,其中 price 設為指定 MenuItem 的更新價格,所有其他資料均保持不變。 |
新增頂層實體 僅適用於 |
Menu 、Restaurant 、Service |
例如,你需要為餐廳新增菜單。 完整動態饋給:在資料動態饋給中新增實體,並等待批次攝入。 |
永久刪除頂層實體 僅適用於 |
Menu 、Restaurant 、Service |
即時更新:傳送明確刪除。 完整動態饋給:請務必在 Google 下次擷取前,從完整動態饋給中移除實體,否則系統會重新加入實體。 |
在特定 Service 中新增運送區域 |
ServiceArea |
批次動態饋給:傳送問題中的 ServiceArea 實體,並保留所有欄位,就像在完整動態饋給中一樣,並在 polygon 、geoRadius 或 postalCode 中指定新的放送區域。 |
更新 Service 的預估送達時間 |
ServiceHours |
批次動態饋給:傳送的 ServiceHours 與動態饋給相同,但其 leadTimeMin 會依據情況更新。 |
更新 Service 中的運送價格 |
Fee |
批次動態饋給:傳送已更新 price 的完整提交 Fee 。 |
在 Service 中更新外送或外帶營業時間 |
ServiceHours |
批次動態饋給:傳送的 ServiceHours 與動態饋給中的相同,但其 opens 和 closes 屬性會依據情況更新。 |
Service (變更最低訂單金額) |
Fee |
批次動態饋給:傳送已更新 minPrice 的完整 Fee |
永久刪除 MenuItem |
Menu |
批次動態饋給:傳送的 MenuItem 與動態饋給中的相同,但 parentMenuSectionId 為空白。 |
批次工作和即時更新的處理時間
透過批次動態饋給更新或刪除的實體,會在 2 小時內處理完畢;透過即時更新功能更新的實體,則會在 5 分鐘內處理完畢。過時實體會在 14 天後刪除。
您可以傳送下列資料給 Google:
- 每天執行多個批次工作,以便隨時更新商品目錄,或
- 每天執行一次批次工作,並即時更新,確保商品目錄保持最新狀態。