iOS中借助Google Web Places API获取地点评分与评论的实现问题
解决Google Places API批量获取门店评分的问题
我来帮你梳理下怎么实现批量获取星巴克门店的评分,正好我之前做过类似的需求:
首先明确两个核心点:
- Nearby Search接口确实不会返回评分、评论这类详细信息,它只提供名称、地址、营业时间等基础字段;
- Place Details接口确实是按单个Place ID查询的,但我们可以通过批量请求的方式来处理,只要注意API的配额和请求速率限制就行。
下面分步骤给你讲具体实现:
1. 先从Nearby Search结果中提取Place ID
首先你需要给你的Store模型添加一个placeId属性,用来存储每个门店的唯一标识:
// 假设你的Store模型结构如下,新增placeId和rating字段 struct Store { let name: String let vicinity: String let lat: Double let lng: Double let openNow: Bool? var rating: Double? // 评分字段,可选类型,兼容无评分的门店 let placeId: String // 新增Place ID字段,用于后续查询详情 }
然后修改你现有解析Nearby Search结果的代码,把place_id提取出来:
// 在loadStarbucksJson方法的解析循环中修改: for result in (results as! NSArray as! [NSDictionary]) { if let geometry = result["geometry"] as? NSDictionary, let location = geometry["location"] as? NSDictionary, let placeId = result["place_id"] as? String { // 提取Place ID let name = result["name"] as! String let vicinity = result["vicinity"] as! String let lat = location["lat"] as! Double let lng = location["lng"] as! Double var openNow : Bool? = nil if let opening = result["opening_hours"] as? NSDictionary { openNow = opening["open_now"] as? Bool } // 初始化Store时传入placeId,rating先设为nil let newStore = Store(name: name, vicinity: vicinity, lat: lat, lng: lng, openNow: openNow, rating: nil, placeId: placeId) NearbyStarbucks.sharedInstance.stores += [newStore] } }
2. 批量调用Place Details接口获取评分
接下来,我们需要遍历所有获取到的Store,逐个调用Place Details接口获取评分。这里要注意两个关键点:
- Google Places API有请求配额限制(免费版配额有限,付费版按请求量计费),同时有速率限制(避免短时间内发太多请求被限流);
- 可以用串行队列或者信号量控制并发数,避免触发限流机制。
下面是一个实现示例,你可以在postLoadStoreJson()方法(或者处理完Nearby Search后的逻辑里)添加批量请求代码:
func fetchStoreRatings() { // 先获取API密钥,和之前的逻辑一致 var apiKey : String = "" if let path = Bundle.main.path(forResource: "keys", ofType: "plist"), let dict = NSDictionary(contentsOfFile: path) { apiKey = dict["webPlacesApiKey"]! as! String } guard let stores = NearbyStarbucks.sharedInstance.stores as? [Store] else { return } // 用信号量控制并发数,一次最多发起3个请求,避免触发限流 let semaphore = DispatchSemaphore(value: 3) let queue = DispatchQueue.global(qos: .background) for (index, var store) in stores.enumerated() { queue.async { semaphore.wait() // 等待信号量,控制并发 // 构造Place Details请求URL,只请求rating字段,节省配额 let urlString = "https://maps.googleapis.com/maps/api/place/details/json?place_id=\(store.placeId)&fields=rating&key=\(apiKey)" guard let url = URL(string: urlString) else { semaphore.signal() // 释放信号量 return } let task = URLSession.shared.dataTask(with: url) { [weak self] data, response, error in defer { semaphore.signal() // 无论成功失败都释放信号量 } guard let data = data, error == nil else { print("获取门店评分失败:\(error?.localizedDescription ?? "未知错误")") return } do { let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] if let result = json?["result"] as? [String: Any], let rating = result["rating"] as? Double { // 回到主线程更新数据和UI DispatchQueue.main.async { NearbyStarbucks.sharedInstance.stores[index].rating = rating self?.tableView.reloadRows(at: [IndexPath(row: index, section: 0)], with: .automatic) } } } catch { print("解析评分数据失败:\(error.localizedDescription)") } } task.resume() } } }
3. 在UI中展示评分
最后,在你的TableView Cell中,判断store.rating是否存在,然后展示对应的内容:
// 在tableView(_:cellForRowAt:)方法里: let store = NearbyStarbucks.sharedInstance.stores[indexPath.row] if let rating = store.rating { ratingLabel.text = String(format: "评分:%.1f/5.0", rating) } else { ratingLabel.text = "暂无评分" }
额外优化建议
- 缓存机制:如果用户多次查看同一区域的门店,可以把Place ID和对应的评分缓存到本地(比如用UserDefaults或Core Data),下次加载时直接使用缓存数据,减少API请求;
- 错误兼容:部分新开门店可能没有评分,要做好UI上的默认展示,避免崩溃;
- 配额监控:在Google Cloud控制台实时监控API使用情况,避免超出配额导致请求失败;
- 密钥安全:虽然你用plist存储密钥的方式是对的,但建议在Google Cloud控制台给API密钥添加iOS包名限制,防止密钥被滥用。
内容的提问来源于stack exchange,提问作者101
相关产品推荐
相关产品推荐

