You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.29 08:02:19