iT邦幫忙

2026 iThome 鐵人賽

DAY 20
0
Build on Google AI

咖啡、Wi-Fi 與 AI:30 天打造數位遊牧工作地圖系列 第 20

Day 20|從一間店開始,試試 Google Places 能提供哪些資料

  • 分享至 

  • xImage
  •  

昨天換上 Google Maps 後,地圖已經能顯示店家位置,不過店名和座標仍來自我們準備的示範資料。今天接著試 Places API,讓一筆真實店家資料出現在 WorkCafe 裡。
這次先用一間店測試,從核對 Place ID、送出查詢,到轉成 WorkCafe 的欄位並顯示在畫面上。Firestore 裡的五間示範店先保留,方便對照。

先分清楚地圖和店家資料

昨天使用的 Maps JavaScript API 負責顯示地圖,今天的 Places API 則用來查店家資料,所以還需要另外啟用。
店家資料原本走的是 Firestore → Laravel → Inertia → React。這次先加一個本機單店測試,由 Laravel 呼叫 Places API,再把必要欄位傳給 React。首頁既有資料與 Google 登入繼續保留。
我先請 Agent 整理接法:

先閱讀 docs/ 的 CURRENT 規格、README、composer.json、package.json,
檢查 FirestoreCafeService、config/services.php、routes/web.php、
Home.tsx、CafeCard、Google Maps 元件與店家型別。
若檔案已更名,找出對應實作。

目前專案是 Laravel+Inertia+React。
店家由 Laravel 讀取 Firestore,前端有 Google Maps 與 Google 登入。

這次要測試一間真實咖啡廳的 Places API (New) 資料,
先列計畫,不修改程式或雲端設定:
1. 如何取得並核對真實店家的 Place ID。
2. Laravel 如何呼叫 Place Details (New),需要哪些 field mask。
3. 回應如何對應 DATA_FIELDS.md,哪些欄位維持未知。
4. 後端金鑰、API 限制、用量與錯誤處理如何安排。
5. 如何建立僅限本機的單店測試畫面,不覆蓋首頁示範資料。
6. 資料儲存、來源標示與計費需核對哪些官方文件。
7. 預計修改檔案與驗證方式。

不要替示範店名猜 Place ID,也不要自動把搜尋第一筆當成正確店家。
不批次匯入、不寫入 Firestore,不新增附近搜尋、評論或照片功能。
不修改登入、既有 Filter 或 Marker 選取行為。
列完計畫後先停下,待確認再實作。

啟用 Places API,準備後端金鑰

打開 Google Cloud Console,選擇原本使用的專案。

  1. 確認專案的帳單設定有效。
  2. 到「API 和服務 → 程式庫」,搜尋並啟用 Places API (New)
  3. 到「憑證」建立供 Laravel 使用的獨立 API key。
  4. API 限制只允許 Places API (New)。
  5. 後端若有固定對外 IP,設定對應 IP 限制;本機沒有固定出口時,先確認可用的限制方式,不拿瀏覽器用的網站限制金鑰代替。

昨天 Maps JavaScript API 的金鑰有網站限制,這次 Laravel 發出的請求則來自伺服器,兩者分開管理。API 金鑰設定
目前 config/services.php 透過 services.google_places.server_api_key 讀取後端金鑰。在專案根目錄的 .env 填入:

GOOGLE_MAPS_SERVER_API_KEY=YOUR_PLACES_API_KEY

YOUR_PLACES_API_KEY 換成剛建立的金鑰。這個值只供後端使用,不加 VITE_ 前綴,也不把完整金鑰傳入 Inertia props。.env 不提交到 Git。設定完成後清除 Laravel 設定快取:

php artisan config:clear

先找到正確的 Place ID

原本示範店的 placeIdnull,不能直接拿來查詢。先找一間能核對名稱、地址與分店的真實店家:

  1. 開啟官方 Place ID 查找說明與工具
  2. 在 Place ID Finder 輸入店名與城市或地址。
  3. 核對分店、地址和地圖位置,再記下 Place ID。
  4. 若改用 Text Search (New),也要先核對候選結果,不能直接採用第一筆。

這次只測一間店。取得 Place ID 後,再請 Agent 建立後面的單店查詢與預覽功能。

只取這次畫面需要的欄位

Place Details (New) 要用 field mask 指定回傳欄位。第一輪先取識別、名稱、地址、座標與必要來源標示:

GET https://places.googleapis.com/v1/places/YOUR_PLACE_ID?languageCode=zh-TW
X-Goog-Api-Key: YOUR_PLACES_API_KEY
X-Goog-FieldMask: id,displayName,formattedAddress,location,googleMapsUri,attributions

這是請求格式示意,實際金鑰由 Laravel 設定讀取。不要把佔位值直接當成可執行的查詢。
Places API (New) 的 nameplaces/PLACE_ID 這種資源名稱;顯示給使用者的店名要讀 displayName.text。營業資訊則另外加入 businessStatuscurrentOpeningHours,先確認所需費用,再執行第二輪請求。Place Details 文件
這組欄位包含 displayName,屬於 Place Details Pro;第二輪加入 currentOpeningHours 後,則屬於 Place Details Enterprise。實際費用依官方計價與用量計算,因此先取需要的欄位,不用 * 全部抓回來。欄位與計費等級

確認計畫後,再請 Agent 實作

核對 Agent 提出的檔案與修改範圍後,再送出以下指令:

請在現有 Laravel+Inertia+React 專案建立單店 Places API (New) 測試。

先讀 docs/ CURRENT 規格、README,檢查現有服務、路由、
CafeCard、Google Maps 元件與店家型別。
沿用現有 Google 登入與 Firestore 首頁資料流程。

實作 Laravel 後端服務與僅限 local 環境的測試入口:
- 接受一個經使用者核對的真實 Place ID,不預填猜測值。
- 沿用 GOOGLE_MAPS_SERVER_API_KEY,透過
  config('services.google_places.server_api_key') 讀取。
  完整金鑰不傳前端、不加 VITE_ 前綴。
- 預設只在使用者明確按「查詢」時呼叫,不隨輸入或 render 重複請求。
- 第一輪 field mask:
  id,displayName,formattedAddress,location,googleMapsUri,attributions
- 營業資訊測試另外明確加入 businessStatus,currentOpeningHours,
  先回報相關計費等級,待確認後才執行該輪請求。
- 不使用 *,設定請求逾時,避免無限制重試。
- 處理金鑰缺少、權限錯誤、找不到店家、額度與網路錯誤。
- 顯示遮蔽金鑰後的請求摘要、field mask、HTTP 狀態、
  本次回應及轉換後的單店卡片和 Google Maps Marker。
- 原始回應只供本機當次檢查,不寫入檔案、日誌或 Firestore。
- 測試入口與查詢端點在非 local 環境不可使用。

欄位轉換:
id 對應 placeId;卡片識別值使用獨立且穩定的測試 ID。
displayName.text 對應 name,formattedAddress 對應 address。
location.latitude/longitude 轉為 location.lat/lng。
沒有請求或沒有回傳的 phone、rating 等欄位保留 null。
businessStatus 的 OPERATIONAL 不得直接當作現在營業中。
若有 currentOpeningHours.openNow,依 boolean 轉成 open/closed;
缺少時維持 unknown,永久或暫停營業另如實呈現。
不把 googleMapsUri 或原始 businessStatus 隨意加進 Cafe 契約,
可用獨立的預覽資料保存顯示所需資訊。

工作條件無查核資料就使用各欄位合法的 unknown 或 null;
不沿用 mock 的插座、Wi-Fi、噪音、限時或會議適用性。
保留 Observation 的 value/source/updatedAt/note 形狀。
真實 Places 測試結果不標成 mock,但要標示為單店測試、
工作條件尚未查核;取回時間不冒充 Google 或人工查核時間。

保留 Google Maps 與回應要求的來源標示。
不做持久快取、不修改既有 Firestore 文件、不新增搜尋或照片評論功能。
列出欄位對照表與實際測試入口網址,說明重新整理結果頁是否會再次請求 API。
執行必要測試、typecheck、build,回報實際結果與未驗證項目。
不輸出私鑰、API key 或 token,不自動 commit 或 push。

開啟測試頁,查第一間店

實作完成後,在原本能開啟 WorkCafe 的本機網址後面加上 /test/place-details。這個入口只在 APP_ENV=local 時提供;若出現 404,先確認環境設定,再執行 php artisan config:clear,並用 php artisan route:list --path=test/place-details 檢查路由。

  1. 貼上剛才核對過的 Place ID。
  2. 第一輪先不勾「加入營業資訊測試」,按下「執行單店查詢」。
  3. 查看請求摘要的 field mask 與 HTTP 狀態。成功時,再比對回應中的店名、地址和座標。
  4. 往下看轉換後的 Cafe JSON、地圖與卡片,確認三者對應同一家店。這輪未查營業時間、電話與評分,畫面顯示未知或未提供是預期結果。
  5. 若要測營業資訊,先確認 Enterprise 計費,再勾選選項並送出第二次查詢;這會產生另一筆 API 請求。

目前測試頁使用 GET 查詢,網址會帶有 place_id。重新整理這個結果頁會再次呼叫 Places API;捲動畫面或調整顯示寬度則不會送出新的 Places 查詢。
若沒有取得結果,先看錯誤訊息:權限錯誤要檢查 API 是否啟用、帳單與金鑰限制;找不到店家就重新核對 Place ID;額度或網路錯誤則先處理原因,再重試。

📸 圖片 1-1|單店查詢與請求摘要
https://ithelp.ithome.com.tw/upload/images/20260921/20121296LGMZtAtru6.png

📸 圖片 1-2|Places 原始回應與 Cafe 資料轉換
https://ithelp.ithome.com.tw/upload/images/20260921/20121296qS2ofzHsJN.png

📸 圖片 1-3|欄位檢查與地圖、卡片預覽
https://ithelp.ithome.com.tw/upload/images/20260921/20121296ZTj85I0xZp.png

對上欄位,再決定畫面怎麼顯示

拿到回應後,先看每個值代表什麼。最容易弄錯的是營業狀態:店家仍在經營,和此刻有沒有開門,是兩個不同的問題。

Places 回應 WorkCafe 對應 處理方式
id placeId 保留 Google 地點識別值
displayName.text name 使用顯示名稱,不取資源名稱 name
formattedAddress address 未回傳時保留 null
location.latitude/longitude location.lat/lng 檢查數值與座標範圍;缺少或無效時,location 為 null
businessStatus business.status、todayClosingText 永久或暫時停業時優先設為 closed,並顯示停業文字;OPERATIONAL 不代表現在營業中
currentOpeningHours.openNow business.status 非上述停業狀態時,true 轉 open、false 轉 closed;缺少為 unknown
未請求或未回傳的評分、電話 rating、phone 保留 null,不補 mock 值
插座、Wi-Fi、噪音、限時、會議適用性 work 各 Observation 的 value 尚未查核,一律保留 unknown
低消 work.minimumSpend.value 尚未查核,保留 null
googleMapsUri、attributions 獨立預覽資料 用於地圖連結與來源標示,不加入 Cafe 欄位

Places 能提供的欄位很多,但不能看到店家類型或評分,就推斷它適合工作三小時。這次沒有取得的工作條件,繼續留待查核。Places 欄位定義

📸 圖片 2|Places 欄位與 WorkCafe 的對照
https://ithelp.ithome.com.tw/upload/images/20260921/201212961emavv6jFN.png

先顯示,再確認哪些資料可以保存

原本 Firestore 的示範資料由我們自行準備,Places 回應則有另外的使用條件,不能因為欄位對得上就整包永久存入。
這次先做當次查詢與顯示,不寫入 Firestore。Place ID 可依官方例外保存;其他內容的快取、保存與使用方式,需逐項核對適用條款。Google 回傳的來源資訊也要跟著顯示,地圖上呈現 Places 結果時使用 Google Map。公開提供服務前,還要準備相應的使用條款與隱私政策。Places 使用與標示要求

📸 圖片 3|單店 Places 資料顯示在測試畫面
https://ithelp.ithome.com.tw/upload/images/20260921/20121296HN5IVATNG4.png

最後核對一次

先執行程式檢查,再回到瀏覽器確認畫面:

php artisan test --filter=GooglePlacesServiceTest
npm run typecheck
npm run build

這組單元測試使用模擬 HTTP 回應,能檢查資料轉換,但不代表真實金鑰與 API 已連通。實際查詢仍要依測試頁的回應確認。

檢查 預期結果
名稱、地址與分店 和人工確認的同一間店一致
地圖 Marker 使用本次回應的座標
未取得的欄位 維持 null 或 unknown
businessStatus 為 OPERATIONAL 不直接顯示現在營業中
API 請求失敗 顯示錯誤,不退回 mock 假裝成功
React 重繪或調整顯示寬度 不重送 Places 查詢
重新整理帶 place_id 的結果頁 目前會再次查詢,不當作單純重繪
未帶 Place ID 開啟測試頁 顯示輸入表單,不呼叫 Places API
Firestore 原有五筆資料 沒有被修改或覆蓋
首頁登入、搜尋、Filter 與 Marker 選取 沿用原本行為

先把一間店的資料接對

昨天讓店家出現在地圖上,今天則把一筆 Places 回應接進既有卡片。核對店名、地址與座標的同時,也能看出哪些欄位還沒有資料。
Places 補上了店家的基本資料,插座夠不夠、環境吵不吵、能坐多久,仍需要另外查核。接下來要整理的,就是這些會影響選店的工作條件。


上一篇
Day 19|把示意地圖換成 Google Maps
系列文
咖啡、Wi-Fi 與 AI:30 天打造數位遊牧工作地圖20
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言