V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
edis0n0
V2EX  ›  PHP

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

  •  
  •   edis0n0 · 2022-12-04 06:49:20 +08:00 · 2852 次点击
    这是一个创建于 480 天前的主题,其中的信息可能已经有所发展或是发生改变。

    经常出现例如用户充值 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 ,之前写这个程序的人已经离职了,临时翻文档学的

    26 条回复    2022-12-06 13:53:59 +08:00
    hobbyliu
        1
    hobbyliu  
       2022-12-04 07:45:33 +08:00
    ```
    if (isset($user->Credits) && $user->Credits != "") {
    $currentCredit = checkStringSign($user->Credits);
    }

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

    我的排查方法是先看 SQL 日志,确定 PHP 提交的 SQL 里的值是 0.994 ,先排除掉是 MySQL 把 1 变成 0.994 的可能,然后看 $paidCredit ,再看 $invoice->credit
    edis0n0
        10
    edis0n0  
    OP
       2022-12-04 08:47:19 +08:00
    @eason1874 #9 查看了 MySQL 日志,确定提交的是 0.994 ,invoice 表里存的是正确的签名后的整数字符串 1 ,弄了一个单独的 PHP 文件 var_dump $paidCredit ,刷新了几次都能正确输出 1
    eason1874
        11
    eason1874  
       2022-12-04 09:14:53 +08:00
    @edis0n0 不好复现的话就先插个眼,等复现了再复盘吧。在 $paidCredit 后,提交 SQL 前,加个判断,发现对不上就阻止提交,提示用户重试,在日志记下所有变量用来复盘
    rekulas
        12
    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
        13
    ydpro  
       2022-12-04 09:42:53 +08:00
    这段代码中确实存在一个 bug 。首先,在检查 $invoice 对象的签名时,应该将金额转换为数字类型,而不是字符串类型。

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

    修改后的代码应该如下所示:
    ydpro
        14
    ydpro  
       2022-12-04 09:43:48 +08:00   ❤️ 1
    $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
        15
    msg7086  
       2022-12-04 09:52:33 +08:00
    @ydpro 笑死,好好的一个聊天 AI 被你们抓来修代码……
    T0m008
        16
    T0m008  
       2022-12-04 11:06:00 +08:00
    `$paidCredit = checkStringSign($invoice->credit);`

    只能是这个 function, 这段里面唯一修改$paidCredit 的
    vacker
        17
    vacker  
       2022-12-04 17:28:58 +08:00 via iPhone
    先不说代码,你这金额好像扣了手续费后的金额
    edis0n0
        18
    edis0n0  
    OP
       2022-12-04 17:47:43 +08:00
    @vacker #17 卧槽真的是这个原因,这个函数还 include 了一个后扣手续费的 PHP 文件 sendEmailNotice.php ,直接改余额和已产生的充值记录,单看文件名完全想不到还做了这事,那里面查询写的有问题经常找不到充值记录所以没扣手续费,不知道多少年了一直有这个问题,都是财务手工加回去的
    edis0n0
        19
    edis0n0  
    OP
       2022-12-04 17:49:53 +08:00
    @edis0n0 #18 查询写的有问题:它只查询有绑定邮箱的用户,没绑定邮箱就找不到用户,没扣手续费,坑死了谁想得到能写成这样
    Rache1
        20
    Rache1  
       2022-12-04 18:50:58 +08:00
    $objDBCD14 ,哈哈,这前面不会还有 $objDBCD1 到 $objDBCD13 吧 😂
    proxychains
        21
    proxychains  
       2022-12-05 09:14:43 +08:00
    @msg7086 这是 AI?
    msg7086
        22
    msg7086  
       2022-12-05 09:49:44 +08:00
    @proxychains 是啊,AI 帮你抓 Bug 。
    lisxour
        23
    lisxour  
       2022-12-05 10:37:39 +08:00
    代码都不用看就知道不是浮点数精度问题,一个正常的语言,怎么可能 3 位小数就开始有精度问题。。。至少都是好几位以上才有这种问题
    Twnysta
        24
    Twnysta  
       2022-12-05 11:00:57 +08:00
    php 有专门的 bc 函数啊,算账都是用这个。直接对比不是自己跟自己不痛快吗?
    vacker
        25
    vacker  
       2022-12-05 11:05:44 +08:00   ❤️ 1
    @edis0n0 微信支付代码写多了,看到 0.994 直觉告诉我这是扣了手续费后的金额,哈哈
    proxychains
        26
    proxychains  
       2022-12-06 13:53:59 +08:00
    @msg7086 已经通过图灵测试了...语气像是客服,有些地方感觉不对劲, 但找不到哪地方不对劲...
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   959 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 25ms · UTC 20:40 · PVG 04:40 · LAX 13:40 · JFK 16:40
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.