昨天換上 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 選取行為。
列完計畫後先停下,待確認再實作。
打開 Google Cloud Console,選擇原本使用的專案。
昨天 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
原本示範店的 placeId 是 null,不能直接拿來查詢。先找一間能核對名稱、地址與分店的真實店家:
這次只測一間店。取得 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) 的 name 是 places/PLACE_ID 這種資源名稱;顯示給使用者的店名要讀 displayName.text。營業資訊則另外加入 businessStatus 與 currentOpeningHours,先確認所需費用,再執行第二輪請求。Place Details 文件
這組欄位包含 displayName,屬於 Place Details Pro;第二輪加入 currentOpeningHours 後,則屬於 Place Details Enterprise。實際費用依官方計價與用量計算,因此先取需要的欄位,不用 * 全部抓回來。欄位與計費等級
核對 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 檢查路由。
目前測試頁使用 GET 查詢,網址會帶有 place_id。重新整理這個結果頁會再次呼叫 Places API;捲動畫面或調整顯示寬度則不會送出新的 Places 查詢。
若沒有取得結果,先看錯誤訊息:權限錯誤要檢查 API 是否啟用、帳單與金鑰限制;找不到店家就重新核對 Place ID;額度或網路錯誤則先處理原因,再重試。
📸 圖片 1-1|單店查詢與請求摘要
📸 圖片 1-2|Places 原始回應與 Cafe 資料轉換
📸 圖片 1-3|欄位檢查與地圖、卡片預覽
拿到回應後,先看每個值代表什麼。最容易弄錯的是營業狀態:店家仍在經營,和此刻有沒有開門,是兩個不同的問題。
| 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 的對照
原本 Firestore 的示範資料由我們自行準備,Places 回應則有另外的使用條件,不能因為欄位對得上就整包永久存入。
這次先做當次查詢與顯示,不寫入 Firestore。Place ID 可依官方例外保存;其他內容的快取、保存與使用方式,需逐項核對適用條款。Google 回傳的來源資訊也要跟著顯示,地圖上呈現 Places 結果時使用 Google Map。公開提供服務前,還要準備相應的使用條款與隱私政策。Places 使用與標示要求
📸 圖片 3|單店 Places 資料顯示在測試畫面
先執行程式檢查,再回到瀏覽器確認畫面:
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 補上了店家的基本資料,插座夠不夠、環境吵不吵、能坐多久,仍需要另外查核。接下來要整理的,就是這些會影響選店的工作條件。