PHP 写的老屎山代码经常出现字符串转浮点数时小一点点的情况,排查了一晚上还没解决,请 v 友帮忙看看

2022-12-04 06:49:20 +08:00
 edis0n0

经常出现例如用户充值 1 元到账 0.994 元这类情况,导致用户余额无法购买商品

以下是充值部分业务逻辑代码:

$paidCredit = checkStringSign($invoice->credit);
if (isset($user->Credits) && $user->Credits != "") {
    $currentCredit = checkStringSign($user->Credits);
}
$currentCredit = $currentCredit + $paidCredit;
$updatedCredit = signString($currentCredit);
$updatePricePlanId = "";
if ($paidCredit > 99.5 && $user->RiskyScore < 1) {
    $updatePricePlanId = ",RiskyScore=0";
}
$query = "UPDATE users SET Credits = '$updatedCredit' $updatePricePlanId WHERE UserId = '$user->UserId'";
$objDBCD14->execute($query);
$comments = $out_trade_no;
$updateQuery = "update payments set Paid=1,TransactionId='$trade_no' WHERE PaymentId = '$out_trade_no' ";
$objDBCD14->execute($updateQuery);
$paymentId = $payments->PaymentId;
$objDBCD14->execute("INSERT INTO topUpRecords SET UserId ='$user->UserId', Credits = '$paidCredit', CreditsLeft = '$updatedCredit', Comments = '$comments'");

$invoice 中记录的金额是正确的 topUpRecords 中记录的金额就变成 0.994 了

但又不是 100%复现,排查了一晚上还没找出问题 checkStringSign 和 signString 接受的参数都是字符串,signString 内部逻辑是把字符串通过简单变换签名后将签名用 .{sign} 的格式附加在末尾,checkStringSign 内部逻辑是根据最后一个.分隔原文和签名,验签成功则返回原文,否则抛出错误,其中都不含显式的 cast 逻辑。

不熟悉 PHP ,之前写这个程序的人已经离职了,临时翻文档学的

3089 次点击
所在节点    PHP
26 条回复
hobbyliu
2022-12-04 07:45:33 +08:00
```
if (isset($user->Credits) && $user->Credits != "") {
$currentCredit = checkStringSign($user->Credits);
}

```
怀疑是这段逻辑的问题,所以才会不稳定复现,打个日志看看呗。
edis0n0
2022-12-04 07:47:48 +08:00
@hobbyliu #1 之前考虑过是这里的问题,但$currentCredit 应该影响不到变量$paidCredit 呀,topUpRecords 里的 Credits 也变成 0.994 了
momocha
2022-12-04 07:53:59 +08:00
把货币*100 换成整数避免浮点数在存储和运算过程中丢失精度
edis0n0
2022-12-04 07:55:20 +08:00
@momocha #3 这套屎山代码修修补补已经运行 13 年,谁敢搞这么大改动
fzlqr091314
2022-12-04 08:13:56 +08:00
用 bcmath
edis0n0
2022-12-04 08:14:52 +08:00
@fzlqr091314 #5 问题是我不确定这个小一点的问题是在哪一步产生的
ysc3839
2022-12-04 08:15:17 +08:00
直接字符串拼接,不怕 SQL 注入的吗?
edis0n0
2022-12-04 08:19:07 +08:00
@ysc3839 #7 反正不是我写的,没提我肯定不敢改
eason1874
2022-12-04 08:23:22 +08:00
这段代码看不出来啥,这段代码唯一动了 $paidCredit 的是 checkStringSign ,而这个函数又没贴出来

我的排查方法是先看 SQL 日志,确定 PHP 提交的 SQL 里的值是 0.994 ,先排除掉是 MySQL 把 1 变成 0.994 的可能,然后看 $paidCredit ,再看 $invoice->credit
edis0n0
2022-12-04 08:47:19 +08:00
@eason1874 #9 查看了 MySQL 日志,确定提交的是 0.994 ,invoice 表里存的是正确的签名后的整数字符串 1 ,弄了一个单独的 PHP 文件 var_dump $paidCredit ,刷新了几次都能正确输出 1
eason1874
2022-12-04 09:14:53 +08:00
@edis0n0 不好复现的话就先插个眼,等复现了再复盘吧。在 $paidCredit 后,提交 SQL 前,加个判断,发现对不上就阻止提交,提示用户重试,在日志记下所有变量用来复盘
rekulas
2022-12-04 09:37:10 +08:00
定位到这一句
$objDBCD14->execute("INSERT INTO topUpRecords SET UserId ='$user->UserId', Credits = '$paidCredit', CreditsLeft = '$updatedCredit', Comments = '$comments'");
将 sql 打印出来,如果金额不对就是上面语句的问题
如果正确继续定位到 db 库提交 sql 到数据库之前,打印 sql 出来看是否正确,如果还正确只能怀疑数据库加了什么机制了
ydpro
2022-12-04 09:42:53 +08:00
这段代码中确实存在一个 bug 。首先,在检查 $invoice 对象的签名时,应该将金额转换为数字类型,而不是字符串类型。

其次,在计算新的积分值时,应该将新支付的积分转换为数字类型,然后再进行加法运算,而不是直接将字符串拼接在一起。

修改后的代码应该如下所示:
ydpro
2022-12-04 09:43:48 +08:00
$paidCredit = checkStringSign($invoice->credit);
$paidCredit = (float)$paidCredit;
if (isset($user->Credits) && $user->Credits != "") {
$currentCredit = (float)$user->Credits;
}
$currentCredit = $currentCredit + $paidCredit;
$updatedCredit = signString($currentCredit);
$updatePricePlanId = "";
if ($paidCredit > 99.5 && $user->RiskyScore < 1) {
$updatePricePlanId = ",RiskyScore=0";
}
$query = "UPDATE users SET Credits = '$updatedCredit' $updatePricePlanId WHERE UserId = '$user->UserId'";
$objDBCD14->execute($query);
$comments = $out_trade_no;
$updateQuery = "update payments set Paid=1,TransactionId='$trade_no' WHERE PaymentId = '$out_trade_no' ";
$objDBCD14->execute($updateQuery);
$paymentId = $payments->PaymentId;
$objDBCD14->execute("INSERT INTO topUpRecords SET UserId ='$user->UserId', Credits = '$paidCredit', CreditsLeft = '$updatedCredit', Comments = '$comments'");

回答来自:From chatgpt
msg7086
2022-12-04 09:52:33 +08:00
@ydpro 笑死,好好的一个聊天 AI 被你们抓来修代码……
T0m008
2022-12-04 11:06:00 +08:00
`$paidCredit = checkStringSign($invoice->credit);`

只能是这个 function, 这段里面唯一修改$paidCredit 的
vacker
2022-12-04 17:28:58 +08:00
先不说代码,你这金额好像扣了手续费后的金额
edis0n0
2022-12-04 17:47:43 +08:00
@vacker #17 卧槽真的是这个原因,这个函数还 include 了一个后扣手续费的 PHP 文件 sendEmailNotice.php ,直接改余额和已产生的充值记录,单看文件名完全想不到还做了这事,那里面查询写的有问题经常找不到充值记录所以没扣手续费,不知道多少年了一直有这个问题,都是财务手工加回去的
edis0n0
2022-12-04 17:49:53 +08:00
@edis0n0 #18 查询写的有问题:它只查询有绑定邮箱的用户,没绑定邮箱就找不到用户,没扣手续费,坑死了谁想得到能写成这样
Rache1
2022-12-04 18:50:58 +08:00
$objDBCD14 ,哈哈,这前面不会还有 $objDBCD1 到 $objDBCD13 吧 😂

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/899919

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX