PHP浮点数字符串转换的坑

问题描述

今天上班,技术客服反馈系统无法给微信支付用户退款,提示:”订单金额或退款金额与之前请求不一致,请核实后再试”。

问题排查

根据提供信息查询日志发现确实有这样的问题。首先排查了订单支付数据没有问题,其次看了是否有没有同事修改了代码。在排查代码的过程中发现关于退款金额的计算有些奇怪,php实现的部分代码如下:

1
2
3
4
5
6
7
8
9
10
<?php
...
return $payment->refund->byOutTradeNumber(
$order->id,
'REFUND' . $order->id,
$fee * 100,
$refund_fee ? $refund_fee * 100 : $fee
);
...
?>

byOutTradeNumber方法的第三、四参数要求是int型,而传递过来的$fee参数是string类型,发生类型强制转换就出问题了。

通过以下示例代码来复现一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php

$fee = "32.80";

var_dump($fee); // string(5) "32.80"
var_dump(floatval($fee)); // float(32.8)
var_dump(floatval($fee) * 100); // float(3280)
var_dump($fee * 100); // float(3280)
var_dump((int)($fee * 100)); // int(3279)
var_dump(round($fee * 100)); // float(3280)
var_dump((int)round($fee * 100)); // int(3280)
var_dump(json_encode(['fee' => $fee * 100])); // string(26) "{"fee":3279.9999999999995}"
var_dump(json_encode(['fee' => round($fee * 100)])); // string(26) "{"fee":3279.9999999999995}"

?>

根据以上结论,在系统调用byOutTradeNumber方法时参数强制装换成int型就会把小数部分去掉,这样就出现金额不一致问题了。其本质原因是:float(32.80)在内存中实际存储的是3279.9999999999995,导致类型装换之后结果不正确。这个在PHP编程中需要特别注意。

解决方法

分析到这里问题解决就容易了,直接上代码:

1
2
3
4
5
6
<?php
...
$fee = (int)round($fee * 100);
$refund_fee = (int)round($refund_fee * 100);
...
?>

ps:PHP语言存在这个问题,其它语言同样也存在这个问题,语言之间语法不同,编程思想、编程经验是通用的。