JSON金额解析BUG的解决过程

本文描述了一次支付系统中出现的金额误差问题及其解决过程。问题出现在金额转换为JSON对象时,导致数据库记录金额出现偏差。通过对JSON解析库的深入研究,最终通过定制化的commons-lang库解决了问题。

【原创申明:文章为原创,欢迎非盈利性转载,但转载必须注明来源】

这是在我们开发的一个支付系统中暴露的一个BUG,问题本身比较简单,有意思的是解决问题的过程。将过程分享出来,希望能够对大家有所帮助。

一、错误现象

在我们的支付系统中,有一个账户模块负责记录交易的流水,以供后续的查询以及对账清账等功能使用。就在春节放假前最后一天,当客户完成交易后,运营同事发现一个天大的问题,流水表中的部分金额,跟提交支付的金额有出入,差了几分钱。

这位客官说了,几分钱的问题,还是问题?哈哈,我也这么想,奈何运营、产品、测试同事们都不答应。好吧,其实我们程序猿是有洁癖的,怎么容忍有这样的问题出现?把火车票、机票都先放在看不见的地方,解决问题先。

先从不同的数据库中找出付款前后的金额进行比较,发现还真不是个案。这是当时比较的结果,黑体部分有差异。

 

这些数据中,业务系统的金额跟客户提交金额相等,账户记录的金额有异。

二、分析并定位问题

1.数据流转过程

下图是一个简略的支付、记录流水的过程。

 

通过检查各个环节的报文及数据库中保存的数据,发现问题出在第4步,金额在支付系统中无误,发送到账户系统并保存到数据库后就出现了误差。这儿发生了什么?

2.账户记账的处理过程

这是一个简略的处理过程,支付系统生成json并传输到账户系统,解析后保存到数据库。

经过查看各个环节的日志,发现问题出在解析环节。

3.错误重现

经过定位、调试,发现问题出在解析json数据的代码上。账户系统接收到传输来的json数据后,首先保存在一个字符串content中,然后利用代码将字符串转换为json对象。

JSONObject json = JSONObject.fromObject(content);

在Eclipse中设置断点跟踪,发现这行代码执行前后的变量值差异:

 

在转换前后,金额从 527726.03 变成了527726,这个差异符合前面观察到的错误现象。仔细查看json字符串,发现金额没有使用双引号括起来,说明生成json的时候,直接赋值的是金额,而不是转成字符串后再赋值。

那么如果将金额用双引号括起来,会有这个问题吗?再测试一下

神奇的是,转换为字符串后,转成json就没有问题了。

我们解析json,使用的是sf的json-lib库,其他json库是不是也有问题呢?使用另两个json库做了一些测试后发现,只有json-lib有这个问题。

 

有问题

<dependency>

<groupId>net.sf.json-lib</groupId>

<artifactId>json-lib</artifactId>

<version>2.4</version>

<classifier>jdk15</classifier>

</dependency>

没问题

<dependency>

<groupId>org.json</groupId>

<artifactId>json</artifactId>

<version>20160212</version>

</dependency>

<dependency>

<groupId>com.google.code.gson</groupId>

<artifactId>gson</artifactId>

<version>2.6.2</version>

</dependency>

 

三、初步解决方案

根据前面的分析,立刻就有了两个很自然的解决方案:修改json中金额的格式、换JSON库。

1.修改json格式

用这个方案,只需要在支付系统中生成json对象的时候,将金额转成字符串之后在赋值到json即可。

但这种方案有缺点,需要将所有生成json的地方都检查一遍,确保所有金额都用字符串传递。因为这个地方代码有问题,其他地方代码也会有问题,只是还没暴露出来而已。

2.替换json库

这种方案,可以将json-lib替换为org.json。暂时不考虑gson,是因为这个gson库需要为json编写对应的Java类,修改工作量比较大。

那么,json-lib和org.json在代码生有什么差异呢?网上找了找,粗略的比较如下:

 

json-lib

org.json

构造 json 对象

 JSONObject.fromObject(content) 

new JSONObject(content)

是否存在key

containsKey()

has()

array方法

 size() 

 add()

length() 

put()

读取json的限制

 

限制数据格式

spring封装

MappingJackson2HttpMessageConverter 支持

貌似缺省不支持

 

这种方案的代码量也是很大,所有涉及到json转换的地方都需要修改代码。如果采用替换json库的方法,有没有更简便一点的做法呢?

把《设计模式》里面的各种名称想了想,“适配器模式”,能不能用上?

3.替换json库+适配器

针对这个方案,做了一些技术预演,大概思路如下图

理想的目标是所有源码只需要使用一次查找-替换操作即可。

 这个方案应该是可行的,只是这两个适配器类的写法需要比较严谨一点,写完代码后需要经过充分的测试无误,才能真正执行。

四、问题解决了吗?

前面提到了三种解决方案,从修改工作量上来看,第一种方案应该是最合适的,只需要修改支付系统的代码即可,代码也容易定位,修改也不容易出错。采用适配器的这个方案,看起来很高大上的样子,但风险较大,暂时先放弃。

还有没有更简单的方法?

1.json-lib为什么会出错?

负责开发账户的同事,下载了json-lib的源码,进行了进一步的跟踪调试,更准确的定位到了出错的位置:是在调用commons-lang.jar中的NumberUtils类中代码时出错。下图是一个简单的调用过程。

最终出错的地方是在解析 Float !!

重新写一个最简单的测试用例,

float floatValue = Float.valueOf("542772.03");

结果,floatValue = 542772.0。这是JDK的Float 数据类型固有的问题,我们同时在JDK1.7和JDK1.8下进行测试,都有这个问题。

同时,顺手写了一个测试用例,找出最小的十个会出错的金额,如下:

error1131072.01131072.02

error2131072.04131072.05

error3131072.07131072.06

error4131072.09131072.1

error5131072.13131072.12

error6131072.15131072.16

error7131072.18131072.19

error8131072.21131072.2

error9131072.24131072.23

error10131072.26131072.27

基本上每过几分钱就会出错。

2.有什么新的解决方案?

能想到两个新的方案

1、修改 java.lang.Float

2、修改 org.apache.commons.lang.math.NumberUtils

这两种方案,技术上可行吗?要从这个思路上去解决问题,需要解决两个问题:

1、能不能修改源码,解决BUG?

2、怎么让修改后的类,生效?

考虑到后续需要讨论的解决方案,先介绍一个大家可能司空见惯但没注意过的概念::ClassLoader

 

3.JVM ClassLoader

参考书目:《深入理解Java虚拟机》,有兴趣的自行阅读。(其实是我也讲不清楚)

① Tomcat中的class 加载顺序

对于普通java类,按照如下优先级进行加载。

l tomcat/webapps/<war>/WEB-INF/classes

l tomcat/webapps/<war>/WEB-INF/lib/*.jar

l tomcat/lib/*.jar

l jre/lib/*.jar

是不是所有的java类都是这个加载顺序?如果可以,我们是不是可以随便重载jdk自己提供的类?

② JRE ClassLoader

Java在设计的时候已经考虑到这个风险,不能允许随便替换JRE自己的类。所以,针对JRE自身的代码,使用的是另一套ClassLoader。对所有java.*和javax.*,使用的加载顺序

 

详细解析,自行查资料吧,我也不懂。

关键是结论:除非我们重写 JRE的jar,才能通过修改 java.lang.Float来解决问题。何况Float的问题,应该不好修改,否则Java早解决了。

3.怎么修改NumberUtils

在NumberUtils,方法 createNumber(String)首先调用createFloat(String)解析,如果抛Exception,再调用createDouble(String)。

有两个自然地修改方案:

1、修改 createNumber(),不再调用 createFloat(),直接调用createDouble()。

2、修改 createFloat(),如果数据解析出错,抛异常。

下面列了一个粗略的修改createFloat(String)的实现,基本思路是解析后再同原字符串做一个比较,如果值不同则抛异常。

public static Float createFloat(String str) {

  if (str == null) {

    return null;

  }

  str = removeZeroTail(str);

  Float floatValue = Float.valueOf(str);

  if (!removeZeroTail(String.valueOf(floatValue)).equals(str)) {

    throw new NumberFormatException(str + " parse float error.");

  }

  return floatValue;

}

 

4.修改后的NumberUtils放哪儿?

根据前面对class loader的分析,修改后的NumberUtils类,有两个保存位置。

① 在账户系统中重写NumberUtils类

将NumberUtils类重写在src/main/java中,部署后在war/WEB-INF/classes下。

如果采用这个方案,需要在所有的项目中重写这个类。

 

③ 重做一个commons-langs.jar

我们使用的版本是2.6,如果能够重做一个新的版本,并让各个项目能方便的引用,这个方案应是最简单的。恰好,我们有内部的Maven库,分享jar不是问题。

五、最终方案:重做commons-lang.jar

1.代码修改

这个就不多说了,Eclipse建一个项目,进行必要的修改,然后打包放到内部maven库中。顺便推荐一个搭建maven内部库的利器:nexus,价格便宜(免费)量又足。当然前提是你需要有一个能够供大家访问的服务器。

2.项目修改方案

各项目修改方案,仅需要修改 pom.xml

① 所有引用了commons-lang的depencency

<dependency>

  <groupId>net.sf.json-lib</groupId>

  <artifactId>json-lib</artifactId>

  <version>2.4</version>

  <classifier>jdk15</classifier>

  <exclusions>

    <exclusion>

      <artifactId>commons-lang</artifactId>

      <groupId>commons-lang</groupId>

    </exclusion>

  </exclusions>

</dependency>

注意exclusion所有的commons-lang老版本引用。

 

② 引用commons-lang的新版本

<dependency>

  <groupId>commons-lang</groupId>

  <artifactId>commons-lang</artifactId>

  <version>2.7.0-SNAPSHOT</version>

</dependency>

六、解决方案的变迁过程

简单列一下方案变迁过程,

1、支付系统修改json格式的封装代码,金额都使用字符串。

2、账户系统替换 json 解析包。

3、写一个 json proxy,从org.json继承,实现json-lib的接口。

4、在项目中重写 NumberUtils工具类。

5、重做一个commons-lang的新版本,各项目引用。

我有时候爱说一句很装的话:一个问题,如果你找到了一个解决方案,那么说明你还没有理解这个问题。

转载于:https://www.cnblogs.com/codestory/p/5512087.html

<think>我们被要求解决关于苹果内购(IAP)支付0元的问题。根据引用的内容,我们没有直接看到关于0元支付的描述,但是我们可以根据IAP的一般问题和优化经验来推断可能的原因和解决方案。首先,我们需要理解什么是支付0元?可能的情况是:1.用户实际支付了0元(例如,在测试环境使用了测试账号,而测试账号允许0元支付?)2.用户支付成功,但是应用端收到的交易凭证中金额为0元(这可能是一个严重的bug)3.或者,支付金额显示为0元,但实际上正常扣款。但根据问题描述,这是一个bug,所以我们主要关注应用端接收到的金额为0的情况。注意:在IAP流程中,支付金额是由AppStore服务器返回的交易凭证(receipt)中包含的。通常,金额是由开发者在iTunesConnect中设置的价格,正常情况下不会是0元。从引用的内容中,我们可以获取以下信息:-引用[4]提到:在测试IAP时,需要注意AppReview的状态。如果应用被拒绝,测试IAP会失败。因此测试环境的状态很重要。-引用[3]中提到:在iTunesConnect中设置产品时,需要设置价格等级(PriceTier),并且产品被设置为“ClearedForSale”(允许销售)。如果价格设置错误,比如价格等级为0,那么支付金额可能是0元吗?实际上,苹果的PriceTier对应的是不同的价格,比如等级1对应0.99美元,等级0是不存在的(最低是等级1)。然而,我们并没有在引用中找到直接解释0元支付的原因。因此我们需要结合IAP的常见问题来排查。可能的原因:1.测试环境使用测试账号,苹果允许测试账号在某些情况下不实际扣款,但交易凭证中金额应该是设定的价格,而不是0元。所以这可能不是0元的原因。2.产品在iTunesConnect中的价格设置错误?但是我们在引用[3]中看到,设置价格时选择的是PriceTier,这些等级都是固定的非0值。3.另一种可能是:在应用内购买时,购买的商品ID与iTunesConnect中设置的商品ID不匹配,导致获取了错误的产品信息,从而显示0元?或者是在应用内硬编码了0元?4.交易凭证验证环节出现问题:应用在接收到交易凭证后,需要发送到苹果服务器验证,然后解析苹果返回的数据。如果解析错误,可能将金额解析为0元。因此,我们可能的解决方案:1.检查商品配置:确保在iTunesConnect中配置的商品价格正确。登录iTunesConnect,进入该应用,在In-AppPurchases中检查每个产品的价格等级(PriceTier)是否正确。确保状态是“Approved”并且“ClearedforSale”是Yes(引用[3])。2.检查应用内商品ID:确保应用内发起的商品ID与iTunesConnect中的ProductID完全一致(引用[3])。不一致可能导致无法获取到正确的价格信息,从而在应用内显示0元(或者显示为免费?)。3.验证交易凭证:确保在应用内正确处理交易凭证。当交易完成后,应用会收到交易凭证,应该将这个凭证发送到自己的服务器,然后由服务器向苹果验证。验证返回的JSON数据中会包含购买的商品ID、金额等信息。如果解析金额时使用了错误的字段,比如解析了0元(例如,可能解析了促销价格字段,而该字段默认为0),则会导致显示0元。因此,检查解析代码。4.测试环境与生产环境:注意引用[4]中提到的,如果应用被拒绝,测试IAP会失败。因此,确保测试环境使用的是沙盒环境,并且沙盒环境账号是测试账号。同时,确保测试时应用的状态不是被拒绝(如果之前提交过被拒绝的版本,按照引用[4]的说法,测试会失败,那么会不会出现0元的情况?不太清楚,但也许会导致其他异常,值得注意)。5.其他可能:检查应用内是否有其他代码错误,例如在显示价格时,将价格硬编码为0元。由于引用中并没有直接说明0元bug,我们只能根据经验给出建议。同时,我们可以参考引用[1]中提到的Keep解决IAP丢单问题的经验:通过多次优化排查问题。因此,我们也需要详细的日志记录来追踪问题。具体步骤建议:第一步:重现问题。确定在什么情况下会出现0元支付。是在测试环境还是生产环境?是所有商品还是特定商品?是特定版本的应用?等等。第二步:检查商品配置。确保iTunesConnect中的商品配置正确。第三步:检查应用内发起购买时的商品信息。在发起购买前,应该先通过SKProductsRequest获取商品信息(包括价格),然后显示给用户。在获取到的SKProduct对象中,有一个price属性,检查这个price是否为0?如果是,说明从AppStore获取到的商品信息就是0元,那么问题可能出在iTunesConnect的设置上(虽然可能性不大,因为价格等级不会是0)。如果获取到的价格正常,那么问题可能出现在交易完成后处理交易信息的环节。第四步:检查交易完成后的处理。在收到交易凭证后,应用需要将凭证发送到苹果服务器进行验证。验证的接口有两种:沙盒环境是https://sandbox.itunes.apple.com/verifyReceipt,生产环境是https://buy.itunes.apple.com/verifyReceipt。验证后,苹果会返回一个JSON,其中包含订单的详细信息。解析这个JSON,特别是字段:-`receipt`->`in_app`->`price`(这个字段表示该商品在购买时的价格,以商品价格所在国家(或地区)的币种表示)-注意:还有`in_app`->`price`可能有多个购买项,需要找到对应交易的那一项。如果这里解析出的价格是0元,那么问题可能出在苹果方面?但这种情况非常罕见。或者,在解析的时候,如果使用了一些错误的路径(比如解析了促销价格`price_consent`?),那么可能导致0元。第五步:如果以上步骤都正常,那么考虑是否在本地代码中覆盖了价格信息。例如,在交易完成后,应用可能再次从本地商品配置中获取价格(比如缓存),而这个缓存的数据有误?特别是在更新商品价格后,如果本地缓存没有更新,可能显示旧价格,但是否有旧价格为0元的情况?第六步:考虑是否与促销相关。iOS11引入了促销优惠,允许开发者发送促销优惠码,用户输入优惠码后可以0元购买?但是这种情况是有意的,不是bug。所以检查交易中是否使用了促销优惠。第七步:检查日志。记录交易过程的每一步,包括:-获取商品信息时的价格-交易凭证中的原始数据(base64编码的receipt)-发送到苹果验证后的返回数据-应用解析后的数据通过日志可以帮助定位问题。最后,如果问题无法解决,可以考虑联系苹果开发者技术支持。由于问题没有给出更具体的场景(例如测试环境还是生产环境,频率如何等),我们只能给出一般性的排错步骤。总结解决方案:1.确认商品在iTunesConnect中的配置(价格等级)是否正确。2.检查应用内获取到的SKProduct对象的价格属性。3.检查交易完成后,向苹果服务器验证凭证后返回的JSON中的价格字段(位于`receipt`->`in_app`->`price`)。4.记录详细的日志,特别是价格信息的变化。5.如果是在测试环境,确保应用没有被拒绝(引用[4]提到被拒绝的应用测试IAP会失败,但失败原因不一定是0元,所以这条只是排除测试环境状态问题)。希望这些步骤能帮助定位并解决问题。</think> ### 苹果内购(IAP)支付0元问题解决方案 根据引用内容分析,支付0元问题可能由以下原因及解决方案构成: --- #### **1. iTunes Connect配置问题** - **原因**:产品价格等级(Price Tier)未设置或错误配置。如引用[3]所述,每种订阅品必须明确设置价格等级,0元支付可能是产品价格未正确配置所致 - **解决方案**: - 登录[iTunes Connect](https://appstoreconnect.apple.com) - 选择对应App → In-App Purchases → 编辑产品 - 检查**Price Tier**是否设置为有效的价格层级(最低为Tier 1=0.99美元) - 确保**Cleared For Sale**状态为`YES`(若为`NO`则产品未上架) - 示例设置: ```diff 价格等级: Tier 1 (¥1.00) // 必须 ≥Tier 1 销售状态: Cleared For Sale ✅ ``` --- #### **2. 应用审核状态异常** - **原因**:引用[4]明确说明,**若App Review拒绝了二进制文件,IAP测试将失败**,可能导致异常支付 - **解决方案**: - 检查iTunes Connect的**App Review状态** - 若被拒绝: - 上传一个不带IAP功能的临时版本通过审核 - 通过审核后立刻回滚版本,再测试IAP功能 - 若在审核中: - 使用**Sandbox测试账号**验证支付流程(生产环境需等待审核通过) --- #### **3. 支付凭证解析错误** - **原因**:客户端解析苹果返回的支付凭证时出现字段匹配错误,错误读取了`price=0`的字段 - **解决方案**: - 验证苹果返回的JSON数据结构: ```json "in_app": { "product_id": "com.yourproduct.id", "price": "1.99", // 正确字段 // 避免错误解析以下字段 "promotional_offer_price": "0" // 促销价字段 } ``` - 在代码中增加价格验证逻辑: ```swift func handlePurchase(receipt: Receipt) { guard let price = receipt.inAppPurchase?.price, price > 0 else { // 触发支付异常处理流程 logError("Invalid price: \(price)") restorePurchase() // 启动订单恢复 return } // 正常处理订单 } ``` --- #### **4. 测试环境误操作** - **原因**:沙盒测试账号在特定条件下可能返回0元账单(如测试账号开启家长控制) - **解决方案**: 1. 创建新的**专用测试账号**(引用[3]要求使用iTunes Connect测试账号) 2. 在设备设置中退出Apple ID → 重新登录测试账号 3. 支付前确认测试环境状态: ```terminal 控制台过滤日志: [IAP] Environment = Sandbox ✅ ``` --- #### **5. 支付流程异步冲突** - **原因**:多线程环境下支付回调与UI更新冲突,导致价格显示未刷新 - **优化方案**: ```swift // 使用主线程确保价格刷新 SKPaymentQueue.default().addTransactionObserver(self) func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) { DispatchQueue.main.async { transactions.forEach { trans in // 强制主线程更新UI updatePriceLabel(to: trans.payment.product.price) } } } ``` --- ### 根因定位步骤 1. **优先检查iTunes Connect**:验证产品价格状态(发生概率>60%) 2. **抓取支付凭证**:通过[验证接口](https://sandbox.itunes.apple.com/verifyReceipt)确认返回的原始价格 3. **隔离测试环境**:使用新创建的标准沙盒账号测试 4. **监控SKPaymentQueue状态**:记录交易状态机转换日志: ``` Purchasing → Purchased → [金额异常时中断] ``` > 引用[1]的优化经验表明:**丢单问题需结合服务器验证日志**,建议在服务端增加价格验证中间件,拦截price≤0的异常订单[^1]。 --- ### 相关问题 1. 如何正确配置iTunes Connect的IAP价格层级? 2. App Review拒绝后如何紧急恢复IAP测试环境? 3. IAP沙盒测试账号有哪些特殊限制? 4. 如何设计IAP支付异常的回溯机制? [^1]: Keep技术团队IAP丢单优化经验 [^2]: iTunes Connect基础操作指南 [^3]: IAP订阅产品与价格配置规范 [^4]: App Review状态对IAP的影响说明
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值