Google Play 无法完成交易排查:五段链路定位与避坑清单
“无法完成交易”这五个字,我在过去两年里见过至少四种完全不同的成因。有的是商品配置没生效,有的是支付资料所在地和账号对不上,还有一次纯粹是测试账号加了白名单却没同步到新包名。这次踩坑的起因很日常:某应用准备在 Google Play 上线一批一次性商品,内部测试阶段一切正常,切到正式轨道后,一部分用户点“购买”直接弹“无法完成交易,请稍后重试”,另一部分用户则能正常走到收银台。同一个包、同一个版本号,结果却分成两拨人——这种“部分人复现”的故障最磨人,因为它既不像配置全错,也不像代码崩了,逼着你把整条支付链路一段段拆开来看。
这篇文章完整记录了那次排查的全过程,从现象分类、链路拆分、逐段验证到最后的复盘清单。适合三类人看:正在接 Google Play 结算库的客户端开发、负责订单校验和发货的服务端同学、以及需要判断“这是不是我们这边的问题”的测试和运营。文中涉及的报错码、参数取舍、配置项都会给出具体说明和计算逻辑,代码部分用 Kotlin 和 Node.js 举例,其他语言照着思路翻译即可。踩过的坑我会尽量原样保留,包括几个当时判断错、后来被日志打脸的地方。
1. 先把现象钉死:三种“无法完成交易”根本不是一回事
1.1 从报错位置区分故障层级
遇到“无法完成交易”,第一反应不应该是去改配置,而是先确定这句话是从哪一层冒出来的。同一条中文提示,在三个位置出现时,含义完全不同。
第一层是客户端 SDK 返回的错误码。结算库回调里BillingResult的responseCode会给你一个明确的枚举值,常见的有ITEM_UNAVAILABLE(4)、BILLING_UNAVAILABLE(3)、DEVELOPER_ERROR(5)、SERVICE_DISCONNECTED(-1)、NETWORK_ERROR(12)。这一层的错误在收银台弹出来之前就发生了,说明连 Play 商店的结算服务都没走通。
第二层是Play 商店收银台弹窗。用户点了购买,收银台正常弹出、选了支付方式、点了确认,然后弹一个“无法完成您的交易,请稍后重试”。这一层说明结算请求已经发到 Play 侧了,问题出在订单创建或支付渠道授权环节。
第三层是发卡行或支付渠道的拒绝。表现形式和上面类似,但通常会在收银台里直接提示“您的付款方式被拒绝”或者引导你更换支付方式。
我当时的现场是第二层:收银台能弹,价格显示正确,币种正确,点确认之后失败。这个信息非常关键——它直接排除了客户端代码、包名、商品查询这一整段,把排查范围压缩到“账号支付资料”和“商品可售状态”两个方向。如果一个团队在没做这个区分的情况下就开始翻代码,大概率会白忙两天。
提示:判断层级最省事的办法是看有没有订单号。Play 侧如果生成了订单,会有一个
GPA.开头的编号,失败时客户端日志里通常能捞到;没有订单号,说明连订单都没创建成功。
1.2 记录现场信息,别靠回忆
那次我做的第一件事是列了一张现场信息表,让测试同学和反馈用户填。不是走流程,是真的有用——后来定位到的那条线索,就是表格里“账号支付资料所在国家”这一栏暴露出来的。
| 信息项 | 采集方式 | 用途 |
|---|---|---|
| BillingResult 返回码 | 客户端日志埋点 | 区分 SDK 层还是收银台层 |
| 失败发生的时间点 | 日志时间戳(统一转 UTC) | 对齐服务端和 Play 侧日志 |
| 账号登录地区 | Play 商店账号设置页 | 判断地区一致性 |
| 支付资料是否完成验证 | 账号支付中心 | 排除支付资料异常 |
| 设备 Play 商店版本 | 设置-应用信息 | 排除客户端过旧 |
| 商品 ID 与类型 | 客户端埋点上报 | 排除商品配置 |
| 是否为测试账号 | 后台白名单比对 | 区分沙盒与真实支付 |
这张表看起来朴素,但它最大的价值是避免把不同原因的失败混成一个问题。当时一共收集到 11 条反馈,按返回码和是否弹收银台一分组,其实只有 3 条是真正的问题,其余 8 条是三类不同的已知限制——一个账号欠款未结清、两个账号属于家庭组儿童成员、五个账号在测试轨道上误用了真实支付方式。如果一开始就把 11 条当成同一个 bug 查,工作量会翻五倍不止。
1.3 复现路径要一次性走通
确定层级之后,第二步是构造一条稳定的复现路径。这里有个容易被忽略的前提:复现用的账号必须和出问题账号的属性一致。用开发者自己的主账号去复现一个普通用户的支付失败,成功率极低,因为开发者账号自带一堆特权(免测白名单、支付资料完整、无家庭组限制),很多限制对它根本不生效。
我的做法是准备三个账号:一个真实的普通账号(走真钱支付)、一个许可测试账号(走测试卡)、一个空白新账号(验证首次支付流程)。三个账号在同一台设备上轮流登录,同一份安装包,跑同一段购买代码。结果很快就有了区分度:普通账号失败、测试账号成功、新账号成功。这个结果把矛头直接指向了“这个特定账号的支付资料状态”,而不是代码或商品配置。
2. 归因框架:把一条支付链路拆成五段
2.1 从点击到发货的完整链路
支付这条链路看起来是一步,实际上是十几个环节串起来的。我把从用户点击“购买”到服务端发货这一段画成了一条线,每一段都有独立的失败可能:
- 客户端调用
queryProductDetailsAsync查询商品,拿到价格和可售状态 - 客户端调用
launchBillingFlow发起购买 - Play 商店校验包名、签名、应用版本是否与后台登记一致
- Play 商店校验商品 ID 是否存在、是否处于可售状态、价格是否覆盖当前地区
- 收银台展示支付方式,用户确认
- 支付渠道(银行卡、运营商代扣、礼品卡余额等)发起授权
- Play 侧创建订单,返回
purchaseToken - 客户端
onPurchasesUpdated回调,把 token 送服务端 - 服务端调用 Play Developer API 校验 token 真实性
- 服务端调用
acknowledge或consume确认交易 - 服务端发货,写订单表
这条线里,第 3 到第 6 段是最容易出问题也最难自查的部分,因为它们全都发生在 Play 侧,你能拿到的只有一句“失败”。但反过来,只要能确认失败发生在第几段,剩下的排查就有明确方向了。
2.2 五段式二分法:先定位再动手
我把上面 11 个环节压缩成五个可独立验证的区段,形成了一套固定的排查顺序。这套顺序的核心逻辑是从近到远、从便宜到贵:先查自己完全可控的,再查需要后台配置的,最后查需要联系支持的。
| 区段 | 覆盖环节 | 自查手段 | 典型耗时 |
|---|---|---|---|
| 应用侧 | 包名、签名、版本、结算库版本 | 本地日志 + 后台配置比对 | 30 分钟 |
| 商品侧 | 商品 ID、状态、价格、税务 | 后台商品列表 + 查询接口返回 | 1 小时 |
| 账号侧 | 支付资料、地区、白名单、家庭组 | 账号设置页逐项核对 | 2 小时 |
| 设备侧 | 商店缓存、多账号、系统时间 | 清缓存 + 换设备交叉验证 | 1 小时 |
| 服务端侧 | token 校验、acknowledge、幂等 | 服务端日志 + API 回放 | 2 小时 |
这张表后来被我做成了团队内部的排查模板,遇到支付问题先按顺序过一遍,八成的问题在前三段就能解决。需要说明的是,这个顺序不是绝对的——如果错误码明确指向某一层(比如DEVELOPER_ERROR基本就是应用侧),可以直接跳过去。但如果没有明确线索,按顺序走比随机试要快得多。
2.3 为什么不用“最小改动法”试错
有同学会问:为什么不直接改一个配置试试,不行再改下一个?这种试错法在小系统里有效,在支付链路上非常危险。原因有三个。
第一,支付配置的变更往往有生效延迟。商品状态改了不是立刻生效,账号白名单加了不是立刻同步,价格改了可能要等缓存刷新。你改完立刻测试,看到失败,会误以为“这个改动没用”,然后继续改下一个,最后改了一堆东西,问题突然好了,但你不知道是哪个改对了。
第二,多个变更叠加之后无法回滚定位。真出问题时,你面对的是一个被改了七八处配置的环境,比原始状态更难排查。
第三,支付相关的很多操作有次数或状态限制。反复用同一个账号发起失败交易,可能触发风控;反复创建删除商品,可能让商品进入异常状态。我见过一个团队为了测试订阅,一天内创建了二十多个 base plan,最后后台列表乱得没法看。
所以我坚持一个原则:一次只改一处,改完记录时间点,等生效窗口过去再测。慢一点,但每次结论都可信。
3. 逐段排查实录:五个区段的具体验证方法
3.1 应用侧:包名、签名和发布轨道最容易踩的三个坑
应用侧的问题最隐蔽,因为它不报错,只是“不生效”。我按可能性从高到低排了三个。
第一坑:签名指纹没登记全。如果应用启用了 Play 应用签名,你本地打测试包用的是上传密钥,Play 商店分发时用的是 Play 重新签名的密钥。这两个 SHA-1 指纹都得登记到后台的 API 访问配置里,只登记一个的话,另一半场景就会校验失败。当时我们后台只登记了 Play 签名的那一个,导致本地直装的测试包调 Play Developer API 时一直 401,但因为客户端有兜底逻辑,表面上看不出来,直到真实支付失败才暴露。
第二坑:版本号低于商店在架版本。Play 商店在发起结算时会拿客户端的versionCode和商店在架版本做比较。如果你的测试包版本号低于线上版本,某些情况下结算会直接失败。这个规则在文档里写得很含蓄,实际踩到才知道疼。规避办法很简单:测试包的versionCode永远设成比线上高。
第三坑:结算库版本与商品类型不匹配。从结算库 7.0 开始,queryProductDetailsAsync必须同时传商品 ID 和商品类型(ProductType.SUBS或ProductType.INAPP),只传 ID 会直接返回DEVELOPER_ERROR。而我们当时用的是 6.x 的写法迁移过来的代码,查询逻辑里类型参数是硬编码的,一次性商品被当成了订阅去查,返回结果为空,前端展示成“商品不存在”,用户点了之后自然失败。
// 错误写法:类型写死成订阅,一次性商品查不到 val params = QueryProductDetailsParams.newBuilder() .setProductList( listOf( QueryProductDetailsParams.Product.newBuilder() .setProductId("coin_pack_100") .setProductType(BillingClient.ProductType.SUBS) // 这里写错了 .build() ) ) .build() // 正确写法:类型由商品配置决定,不要硬编码 fun buildProductParams(id: String, type: String) = QueryProductDetailsParams.Product.newBuilder() .setProductId(id) .setProductType(type) // INAPP 或 SUBS .build()改完之后查询正常了,但收银台还是弹“无法完成交易”。说明应用侧只是问题之一,不是全部。这里要提醒一句:排查过程中修掉的每一个问题,都要单独记录、单独验证,不要因为“修完还是失败”就认为这个修复没意义。它可能只是被后面更严重的问题盖住了。
3.2 商品侧:状态、价格覆盖和税务设置
商品侧的验证相对直接,因为后台能看到状态。但有几个细节是后台界面不提示、只有出错才发现。
商品状态必须是 Active。一次性商品有 Draft 和 Active 两种状态,Draft 状态下客户端查询能拿到商品信息(这点很坑,它不报错),但发起购买时收银台会失败。我们那次有一批商品是先批量创建的,创建完忘了批量激活,其中两个 ID 一直停在 Draft,恰好就是反馈用户点的那两个。
价格必须覆盖目标地区。Play 支持按国家/地区设置不同价格,但如果你只设了少数几个地区,其他地区的用户看到的可能是空白或者直接失败。检查方法是在后台商品详情页看“价格”那一栏有没有出现未设置地区的警告。当时我们的商品是用美元定价、让系统自动换算的,理论上应该全覆盖,但有两个地区的换算结果低于最低价格限制,系统没有自动补,就留了空。
税务设置会影响结账。这个是我之前完全没意识到的点。部分国家/地区要求应用在后台配置税务类别,没配置的话收银台在某些地区会直接拒绝结账,报错还特别含糊。后台的“税务设置”入口比较深,建议上线前统一过一遍。
生效延迟要有心理预期。商品新建或修改后,同步到全部地区通常需要几分钟到几小时不等。我的做法是改完之后等 30 分钟再测,并且在测试前先用服务端调一次商品查询接口确认状态,避免在没生效的时候白测一轮。
// 用服务端接口确认商品当前状态,比看后台界面更可靠 const { google } = require('googleapis'); async function checkProductStatus(packageName, productId) { const auth = new google.auth.GoogleAuth({ scopes: ['https://www.googleapis.com/auth/androidpublisher'], }); const client = await auth.getClient(); const publisher = google.androidpublisher({ version: 'v3', auth: client }); const res = await publisher.monetary.products.get({ packageName, productId, }); // 重点关注 listings 是否覆盖目标地区、prices 是否有空缺 return { sku: res.data.sku, status: res.data.status, // 期望是 active defaultPrice: res.data.defaultPrice, prices: res.data.prices, // 逐地区检查是否有缺口 }; }这段代码后来被我加进了上线前的自动检查脚本,跑一遍就知道有没有漏配的地区。它比人工点后台靠谱得多,尤其是商品数量上了两位数之后。
3.3 账号侧:这次真正的元凶
前面两段修完,问题范围缩小到了账号。这里是我花时间最多、也最有收获的一段。
支付资料必须完成验证且地区一致。每个 Play 账号都有一个支付资料(Payments Profile),里面记录了账号所在的国家/地区和结算币种。如果这个资料没完成验证(比如缺地址、缺身份信息),或者资料里的国家和地区与当前账号实际使用地区不一致,收银台在创建订单时就会失败。这次出问题的账号就是这种情况:账号本身注册在 A 地区,支付资料填的是 B 地区,两边的货币结算规则对不上,Play 侧直接拒绝创建订单。
判断方法不复杂:让用户打开 Play 商店的付款和订阅设置,看支付资料那一栏有没有“需要验证”或者“信息不完整”的提示。有的话,补全之后一般等十几分钟就能恢复。这里要注意,补全资料不等于立刻生效,我实测过几次,快的时候几分钟,慢的时候要到第二天。
测试账号白名单有严格的使用边界。许可测试账号只能在内部测试、封闭测试这类非正式轨道上使用测试支付方式。如果把测试账号放到正式轨道,或者用了真实支付方式,行为是不可预期的——有的能成功,有的直接失败。当时有两条反馈就是测试同学在自己的正式版上操作导致的误报,白白占了两天排查时间。
账号欠款会阻断新交易。这个坑很冷门。如果账号之前有未完成的退款、拒付或者余额为负,Play 会暂停该账号的新购买行为,表现就是“无法完成交易”。后台看不到这个状态,只能让用户自己在支付中心查看。
家庭组和儿童账号有限制。家庭组成员购买需要家庭管理员批准,儿童账号更是完全走审批流程。这类失败通常会有单独的提示,但有些机型和语言下提示会被吞掉,只留一句通用错误。
| 账号异常类型 | 用户侧表现 | 自查入口 | 恢复方式 |
|---|---|---|---|
| 支付资料未验证 | 收银台创建订单失败 | 付款和订阅设置 | 补全资料并等待同步 |
| 资料地区与账号不符 | 同上,且换支付方式无效 | 支付资料详情页 | 修正地区信息 |
| 存在欠款或负余额 | 所有新购买被拒 | 支付中心账单页 | 结清欠款 |
| 测试账号用在正式轨道 | 行为不可预期 | 后台测试账号列表 | 切回测试轨道 |
| 家庭组儿童成员 | 需管理员批准 | 家庭组设置 | 由管理员完成购买 |
这张表是我后来整理的,当时是一条一条试出来的。有了它之后,客服同学遇到类似反馈可以先自己过一遍,不用每次都拉开发。
3.4 设备侧:缓存、多账号和系统时间
设备侧的问题占的比例不高,但排查成本最低,属于“顺手就能排除”的一类,所以我一律放在前面做。
Play 商店缓存过期是最常见的。商品信息、账号状态在设备上都有本地缓存,缓存不一致时会出各种奇怪的表现。清缓存的操作路径是设置里找到 Play 商店应用,清除缓存(不是清除数据,清数据会把账号登录状态也清掉,得重新登录)。清完重启商店,再试一次。
多账号登录是另一个高频原因。一台设备上登录了多个 Play 账号时,收银台可能用了不是你预期的那个账号去结算。表现就是“我明明有余额却提示失败”。解决方法是进 Play 商店切到正确的账号,或者在收银台确认页仔细看当前用的是哪个账号。
系统时间偏差会影响请求签名校验。如果设备时间误差超过一定范围,服务端校验会失败。这个在小众机型和手动改过时间的设备上偶发,检查一下自动时间同步是否开启即可。
商店版本过旧也值得看一眼。老版本商店对新的结算接口支持不完整,尤其是一些长期没更新过商店的设备。这个没法让用户自己解决,只能提示更新。
3.5 服务端侧:acknowledge 超时是最容易被忽略的坑
服务端这一段,我原本以为不会有问题,结果它贡献了一个“看起来像失败、其实是后续失败”的经典案例。
Play 的规则是:一次性商品在购买成功后,必须在三天内调用acknowledge或者consume确认交易,否则订单会被自动退款。订阅则是必须在三天内确认,否则订阅会被取消。这个机制本身不复杂,但它导致的现象很容易误导人:用户第一次购买成功,扣了钱,但因为服务端没确认,三天后自动退款。用户再买的时候,账号处于某种异常状态,收银台报“无法完成交易”。
我们那次的代码里,acknowledge调用是写在发货成功之后的,而发货逻辑依赖一个第三方接口,那个接口偶尔超时。超时的时候,代码直接抛异常返回,acknowledge根本没执行。结果就是少数订单静默退款,用户反复购买反复失败。
// 错误顺序:先发货再确认,发货失败就永远不确认 async function handlePurchase(purchaseToken, productId) { await deliverProduct(productId); // 第三方接口,可能超时 await acknowledgePurchase(purchaseToken); // 可能永远执行不到 } // 正确顺序:先确认再发货,发货失败进入重试队列 async function handlePurchaseSafe(purchaseToken, productId) { // 先确认,锁定交易,避免三天后自动退款 await acknowledgePurchase(purchaseToken); // 发货失败不影响交易有效性,进入重试队列补偿 try { await deliverProduct(productId); await markDelivered(purchaseToken); } catch (err) { await enqueueRetry(purchaseToken, productId, err.message); } }顺序调换之后,退款问题消失了。这里的原则是:交易确认和权益发放是两件事,确认要快,发放可以慢,但不能因为发放慢而丢了确认。
另外还有一个幂等的问题。同一个purchaseToken可能因为客户端重试、网络抖动被服务端收到多次。如果没做幂等,用户会重复拿到权益。我的做法是拿purchaseToken做主键建唯一索引,插入冲突就直接返回成功,不再重复发放。
// 用唯一索引做幂等,简单可靠 async function recordOrder(purchaseToken, orderId, productId, userId) { try { await db.query( `INSERT INTO orders (purchase_token, order_id, product_id, user_id, status) VALUES ($1, $2, $3, $4, 'pending')`, [purchaseToken, orderId, productId, userId] ); return { isNew: true }; } catch (err) { if (err.code === '23505') { // PostgreSQL 唯一约束冲突 return { isNew: false }; } throw err; } }4. 高频坑位速查表与避坑经验
4.1 一张表覆盖八成故障
排查完之后我把整个过程整理成了一张速查表,贴在团队文档首页。它的用法是:拿到反馈先看返回码,对上行,按“优先检查”那一列的顺序验。
| 返回码或现象 | 最可能原因 | 优先检查 | 验证方法 |
|---|---|---|---|
ITEM_UNAVAILABLE(4) | 商品未激活或 ID 写错 | 后台商品状态 | 调商品查询接口看 status |
BILLING_UNAVAILABLE(3) | 账号不支持结算 | 账号地区与支付资料 | 换账号交叉验证 |
DEVELOPER_ERROR(5) | 参数错误或签名不匹配 | 商品类型参数、签名指纹 | 看客户端日志 debugMessage |
SERVICE_DISCONNECTED(-1) | 商店服务异常 | 商店版本与缓存 | 清缓存后重试 |
NETWORK_ERROR(12) | 网络中断 | 客户端网络状态 | 换网络环境重试 |
| 收银台弹出但确认失败 | 支付资料或商品价格 | 支付资料验证状态 | 换支付方式对比 |
| 购买成功后三天退款 | acknowledge 未执行 | 服务端确认调用链 | 查订单状态与退款记录 |
| 部分用户能成功部分失败 | 账号属性差异 | 白名单、家庭组、欠款 | 按账号属性分组验证 |
这张表最有用的一列其实是“验证方法”,因为它把每个判断都变成了可执行的动作。很多排查卡住不是因为不知道原因,而是不知道怎么确认自己的猜测对不对。
4.2 几条用血换来的规矩
第一条:任何与支付有关的配置变更,都要记录时间点。改了什么、什么时候改的、预计多久生效,写在一张共享表格里。我们后来因为没记录,在同一个商品上重复改了三遍,每次都不知道前面改的到底生效了没有。
第二条:测试账号和真实账号要严格分开用。测试账号上能跑通,不代表真实账号能跑通;反过来也一样。每次验证都要两个都跑,缺一不可。这一点在我后来的项目里救过好几次场——有个 bug 只在真实账号出现,测试账号完全无感。
第三条:别在周五下午改支付配置。生效延迟、缓存刷新、后台同步这些不可控因素,在没人盯着的时候出问题最麻烦。支付链路的变更尽量安排在周一到周三,留出足够的观察窗口。
第四条:服务端永远不要相信客户端的购买结果。客户端回调的purchaseToken必须拿去 Play Developer API 校验一遍,确认商品 ID、购买状态、订单号都对得上才能发货。这一条是常识,但确实见过为了省一次请求而不校验的实现,后果是被人用伪造回调刷了一堆道具。
第五条:给 acknowledge 加监控。不管你的代码写得多稳,都要有一个定时任务去扫“已购买但未确认”的订单,超过 24 小时还没确认的立刻告警。三天的窗口看着长,实际上一忙就忘,等退款通知来了才想起来。
5. 上线前的回归清单与监控埋点
5.1 一份可以照着执行的检查清单
这次踩坑之后,我整理了一份上线前必过的清单。它不是泛泛的“检查配置”,每一项都对应一个具体的操作和预期结果。
- 包名与签名:确认 Play 应用签名和上传密钥签名两个 SHA-1 都登记在 API 访问配置里
- 版本号:测试包
versionCode高于商店在架版本 - 结算库版本:确认与当前商品类型兼容,查询参数带类型
- 商品状态:全部为 Active,无 Draft 遗留
- 价格覆盖:逐地区检查无空缺,无低于最低限价的地区
- 税务设置:目标地区全部配置完成
- 测试账号:白名单正确、仅在测试轨道使用
- 服务端确认:acknowledge 在发货前调用,有重试和告警
- 幂等处理:purchaseToken 唯一索引已建
- 校验接口:服务端调 Play Developer API 校验 token 的链路已联调
清单的执行方式也有讲究:不能一个人从头跑到尾。至少要有一个人用普通用户账号、一台干净设备跑一遍完整流程,因为这个视角最容易暴露开发者视角看不见的问题。
5.2 埋点要埋到能定位问题的颗粒度
日志埋点这块,我最初只记了“购买成功/失败”两个状态,出问题时完全不够用。后来改成了四个字段一起上报:responseCode、debugMessage、productId、orderId。debugMessage这个字段特别关键,它里面经常带着 Play 侧返回的原始错误描述,比返回码具体得多。
// 购买回调里把关键信息一次性打全 override fun onPurchasesUpdated(billingResult: BillingResult, purchases: MutableList<Purchase>?) { val code = billingResult.responseCode val msg = billingResult.debugMessage val orderId = purchases?.firstOrNull()?.orderId ?: "none" val productId = purchases?.firstOrNull()?.products?.firstOrNull() ?: "none" analytics.track("purchase_result", mapOf( "code" to code, "debug_message" to msg, "order_id" to orderId, "product_id" to productId, "billing_lib_version" to BillingClient.VERSION )) }除了客户端埋点,服务端也要有自己的观测点:每小时统计一次“创建订单数”和“确认成功数”的差值,差值异常升高就说明有订单卡在中间环节。这个指标比单纯的失败率灵敏得多,因为它能在用户投诉之前就发现问题。
最后再分享一个我在实际排查里养成的小习惯:每次排查完,把当时的错误判断也记下来。这次我一开始笃定是网络问题,花了大半天去看客户端重试逻辑,结果完全跑偏。把这个错误的判断写进复盘文档,下次再遇到类似症状,至少能少走一次弯路。支付这条链路涉及的东西太多,没人能一次就猜对,但可以让每次猜错的成本越来越低。