复盘微信支付金额不正确问题解决过程——PHP浮点型计算
问题
2017年9月份,商城项目在运行过程中,购买某商品时如果在下单时没有完成付款,而是稍后再从“个人中心-我的订单”发起付款,则无法调起微信支付界面
思路
- 其他商品正常,说明导致问题的原因大概率是商品本身
- 只有从会员中心发起的付款存在此问题,说明大概率是会员中心的代码存在问题
- 需要先观察问题出现时“统一下单”是否能够成功,检查是否是参数问题导致订单无法在微信端创建
观察统一下单返回值
result_code=FAIL err_code=OUT_TRADE_NO_USED err_code_des=商户订单号重复
微信官方对于此问题的描述如下:
出现这个问题的时候建议核查订单号是否重复提交,但实际上在这个使用场景下,我们是“故意”重复提交订单号的。因为从会员中心发起支付的时候订单已经创建了,系统会再次请求微信统一下单接口,即便如此,我们也没有必要每一次请求支付都创建一个新的订单号。
那为什么返回了这个错误
我先给出结论再描述排错过程:
所谓的同一笔交易不能多次提交,实际上指的是在商品描述、标价金额不相同的情况下,用同一个订单号访问了统一下单接口。
这里的错误实际上是因为:从会员中心发起支付时“标价金额”与提交订单时的不相同。
PHP浮点型运算
以下是某位程序员写的微信支付代码:
$total_fee = (int)($order_total * 100);
微信要求金额的单位必须为分,而数据库中订单金额单位是元,所以使用订单金额*100是正确的做法。
订单支付金额的计算非常复杂,所以单位转化为分之后再转化为整型,可以保证微信支付参数不出错,也是正确的做法。
但这里面隐藏了一个问题,还记得我们问题发生的条件必须是“购买某商品时”吗?如果单独购买这个商品的话,订单的金额是19.9。我们可以尝试:
echo (int)(19.9 * 100); // 结果为1989,而非1990
这就导致了订单创建时给微信的支付数据是1990,而再次支付时却是1980,所以接口返回了“订单号重复”的错误。
为什么会少了1分钱呢?PHP的官方文档中是这么说:
随后我又实验了很多数字,结果如下:
echo (int)(19.1 * 100);// 1910 echo (int)(19.2 * 100);// 1920 echo (int)(19.3 * 100);// 1930 echo (int)(19.4 * 100);// 1939 注意这里出现了问题 echo (int)(19.5 * 100);// 1950 echo (int)(19.6 * 100);// 1960 echo (int)(19.7 * 100);// 1970 echo (int)(19.8 * 100);// 1980
这个问题的产生,似乎存在规律,例如19.4、18.4和17.4转化后是错误的,而8.4转化后返回了正确的结果。
更有趣的是:
echo (int)((19.8+0.1) * 100); // 1990 注意此时结果是正确的 var_dump(19.9 == (19.8 + 0.1)); // false
调试过程
实际上这种奇怪的问题排查起来没有什么捷径,无非就是打日志追踪变量,最多也就是细心点罢了。
最终使用了一个比较讨巧的方式解决了这个问题,将代码改为了:
$total_fee = (int)(($order_total + 0.00001) * 100);
至于更加严谨的浮点数计算方法,今后遇到的时候,再研究吧。
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
Java中死锁的定位与修复
死锁应该可以说是并发编程中比较常见的一种情况,可以说如果程序产生了死锁那将会对程序带来致命的影响;所以排查定位、修复死锁至关重要; 我们都知道死锁是由于多个对象或多个线程之间相互需要对方锁持有的锁而又没有释放对方所持有的锁,导致双方都永久处于阻塞状态; 如上图所示,线程1持有对象1的锁、线程2持有对象2的锁,持此线程1又想去获取对象2对象锁、线程2想获取对象1对象锁,此时由于双方都没有获取到想要的锁,任务没完成所以也没释放锁,导致一直僵持呢,于是阻塞、产生死锁; 死锁检测 需要检测死锁肯定要先有死锁出现,下面的demo模拟了一个死锁的产生; public class DeadlockDemo extends Thread { private BaseObj first; private BaseObj second; public DeadlockDemo(String name, BaseObj first, BaseObj second) { super(name); this.first = first; this.second = second; } public void r...
- 下一篇
[译文]c# /.Net 技巧: ToDictionary() and ToList()
原文: [译文]c# /.Net 技巧: ToDictionary() and ToList() 前言: 有两个简单好用的LINQ扩展方法ToDictionary()和ToList(),你可能知道或不知道,但是它的的确确可以简化查询转化为集合的任务: 简介:LINQ和延迟执行 据你所认识的LINQ,你可能会不知道这些查询表达式在幕后做了些什么。让我们说说今天我们示例的目的,我们有一些POCO类(POCO代表传统CLR对象,指的是一个类,它只有非常少的功能,这一概念源自JavaPOJO)。 1 // just a simple product POCO class. 2 public class Product 3 { 4 public string Name { get; set; } 5 public int Id { get; set; } 6 public string Category { get; set; } 7 } 非常简单的类,对吗?我不是说程序需要如此简单,只是专注于LINQ本身,而且我们不一定要真正查询。所以,在我们的程序中我们可以构建一个简单的例子,这些对象的集合...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
-
Docker使用Oracle官方镜像安装(12C,18C,19C)
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- CentOS8编译安装MySQL8.0.19
- Docker快速安装Oracle11G,搭建oracle11g学习环境
- SpringBoot2配置默认Tomcat设置,开启更多高级功能
- MySQL8.0.19开启GTID主从同步CentOS8
- CentOS7,8上快速安装Gitea,搭建Git服务器
- Jdk安装(Linux,MacOS,Windows),包含三大操作系统的最全安装
- SpringBoot2编写第一个Controller,响应你的http请求并返回结果
推荐阅读
最新文章
- MySQL8.0.19开启GTID主从同步CentOS8
- Windows10,CentOS7,CentOS8安装MongoDB4.0.16
- Docker使用Oracle官方镜像安装(12C,18C,19C)
- SpringBoot2编写第一个Controller,响应你的http请求并返回结果
- CentOS8编译安装MySQL8.0.19
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- SpringBoot2整合Redis,开启缓存,提高访问速度
- CentOS关闭SELinux安全模块
- Hadoop3单机部署,实现最简伪集群
- CentOS6,7,8上安装Nginx,支持https2.0的开启