Google Maps API 使用 sessiontoken 后仍单独计费
结论
sessiontoken 的使用方式基本正确。同一次输入过程中应复用一个 token。用户选择预测结果后,把同一个 token 传给 Place Details;会话结束后,再生成新的 token。
Google Cloud Console 的 API usage 显示 9 次请求,不等于按 9 个独立会话收费。Usage 记录的是实际发出的 HTTP 请求。sessiontoken 不会把多次网络请求合并成一次,控制台也不会因此只显示一次调用。它的作用是让 Places API 后端关联这些请求,并按会话归类和计费。
“整个流程只按一次请求收费”也不一定表示 Autocomplete 和 Place Details 合计只产生一项费用。实际收费取决于使用的是 Places API(Legacy)还是 Places API (New),还与 Place Details 请求的字段及当前价格规则有关。
为什么控制台仍显示多次调用
下面的过程本来就会产生多次 HTTP 请求:
Autocomplete: input=s
Autocomplete: input=st
Autocomplete: input=sta
...
Autocomplete: input=station
Place Details
所以 API usage 分别记录每次请求是正常的。sessiontoken 只是把这些调用关联到同一个会话:
一个用户输入会话
├── 多次 Autocomplete 请求
└── 一次 Place Details 请求(结束该会话)
确认计费是否正确时,不要只看 API usage 中的请求数。应查看 Billing 里按 SKU 统计的用量和费用,例如 Autocomplete session、Autocomplete request、Place Details 等相关 SKU。具体名称可能会随 API 版本和 Google 的价格体系调整。
推荐的调用流程
用户每次开始新的地点搜索时:
- 创建一个全局唯一且不可预测的 session token。
- 本轮输入产生的所有 Autocomplete 请求共用这个 token。
- 用户选择预测结果后,使用该结果的
place_id调用 Place Details。 - Place Details 传入完全相同的 token。
- Place Details 完成后立即作废这个 token。
- 下一轮搜索创建新的 token。
- 如果用户取消搜索,没有选择任何结果,也应结束当前会话,不要把 token 留到下一轮继续使用。
示例:
import 'package:uuid/uuid.dart';
const uuid = Uuid();
String? sessionToken;
void startAutocompleteSession() {
sessionToken = uuid.v4();
}
Uri buildAutocompleteUri(String input) {
sessionToken ??= uuid.v4();
return Uri.https(
'maps.googleapis.com',
'/maps/api/place/autocomplete/json',
{
'input': input,
'key': apiKey,
'sessiontoken': sessionToken!,
'location': '22.863185,87.3552233',
'radius': '5000',
'strictbounds': 'true',
},
);
}
Uri buildPlaceDetailsUri(String placeId) {
if (sessionToken == null) {
throw StateError('No active autocomplete session');
}
return Uri.https(
'maps.googleapis.com',
'/maps/api/place/details/json',
{
'place_id': placeId,
'key': apiKey,
'sessiontoken': sessionToken!,
// Legacy Place Details 可通过 fields 限制返回内容。
'fields': 'place_id,name,geometry,formatted_address',
},
);
}
void finishAutocompleteSession() {
sessionToken = null;
}
调用顺序应类似:
startAutocompleteSession();
final autocompleteUri1 = buildAutocompleteUri('sta');
final autocompleteUri2 = buildAutocompleteUri('stat');
final autocompleteUri3 = buildAutocompleteUri('station');
final detailsUri = buildPlaceDetailsUri(selectedPlaceId);
try {
await fetchPlaceDetails(detailsUri);
} finally {
finishAutocompleteSession();
}
补充日志中的两组请求使用了不同的 token,而且每组最后都通过一个使用相同 token 的 Place Details 请求结束。从调用结构看,这样处理是合理的。
需要检查的关键点
1. 参数名称必须与 API 版本匹配
日志使用的是 Legacy Places Web Service 端点:
/maps/api/place/autocomplete/json
/maps/api/place/details/json
对应的查询参数是:
sessiontoken
Places API (New) 的端点、请求方式、token 字段位置和计费规则都不同,不能直接沿用 Legacy API 的示例或旧版价格说明。
2. Place Details 必须来自本次预测结果
用于结束会话的 Place Details 请求,应传入本次 Autocomplete 返回且由用户选中的 place_id。不要随意选择一个地点 ID 发起 Details 请求来“关闭”会话。
3. 一个 token 只对应一次用户搜索
遇到以下情况时,不要继续复用旧 token:
- 用户已经选择地点并完成 Place Details;
- 用户关闭或清空搜索框;
- 用户开始一轮语义上独立的新搜索;
- 多个用户或多个并发搜索控件同时工作。
并发输入框需要分别维护各自的 token。不要用单例 token 混合多个会话。
4. 不要为每个按键生成 token
下面的写法是错误的:
Future<void> autocomplete(String input) async {
final token = const Uuid().v4(); // 每次输入都生成新 token
// ...
}
token 应在搜索会话开始时生成,而不是在每次发送 Autocomplete 请求时生成。
5. 控制请求频率
即使使用了 session token,也应对输入事件进行 debounce,减少无意义的请求:
Timer? debounce;
void onSearchTextChanged(String value) {
debounce?.cancel();
debounce = Timer(const Duration(milliseconds: 300), () {
if (value.trim().isNotEmpty) {
requestAutocomplete(value);
}
});
}
这样既能减少请求量,也能防止旧响应覆盖新响应。生产代码还应取消已经过期的请求,或者通过递增序号忽略乱序返回的结果。
如何确认实际计费
应在 Google Cloud Billing 报表中查看对应项目和 Places SKU,不能根据 API usage 的请求数直接推断费用:
- 确认 Autocomplete 和 Place Details 使用的是同一 Google Cloud 项目下的凭据。
- 在 Billing Reports 或 Cost Table 中按 SKU 分组。
- 把筛选时间范围调整到请求发生后的完整结算周期。
- 分清“请求数量”“会话数量”和“计费用量”。
- 确认当前采用的是 Legacy 还是 New API 的价格规则。
计费数据可能延迟。请求刚完成时查看控制台,相关数据不一定已经归类完毕。
如果 Billing 报表确实将同一 token 下的所有 Autocomplete 调用都计入按请求收费的 SKU,应继续检查是否混用了不同 API 版本或不同项目的 API key,token 是否无效或过期,以及 Details 请求是否成功完成。仅凭 API usage 显示 9 次调用,无法判断 session token 是否生效。
备注:内容仅供参考。
