fastAdmin支付插件之异步回调问题,处理并发通知

最近使用FastAdmin的支付插件来开发了一个PC端扫码充值余额的功能。在成功充值后,我遇到了微信服务器多次发送异步回调通知,我的数据多次修改的问题。我按照FastAdmin支付插件里的回调响应,

//下面这句必须要执行,且在此之前不能有任何输出
return $pay->success()->send();

但是我觉得这一行代码没有实际作用(不知道是否是bug,网上没找到相关解决的办法),因此没有向微信服务器发送成功的响应消息使微信不再推送。这导致微信服务器继续发送通知,最终导致我的数据被多次修改,以下内容主要是解决这个问题。

我查阅了微信支付官方文档,发现文档明确指出:“同样的通知可能会多次发送给商户系统。商户系统必须能够正确处理重复的通知。” 推荐的做法是,当商户系统收到通知后,首先检查相关业务数据的状态,判断通知是否已经被处理。如果尚未处理,则进行处理;如果已经处理,则直接返回处理成功的结果。在处理业务数据之前,必须使用数据锁来进行并发控制,以防止函数重入造成数据混乱。

并发控制确实是需要考虑的问题。下面是我打印的日志:

写入时间:	2023-09-15 10:07:21
支付时间:	2023-09-15T10:07:20+08:00
商户订单号:	R20230915100609000017
支付方式:	wechat
微信支付系统订单号:	4200001944202309159886018410
支付类型:	NATIVE
总金额:	0.01
用户支付金额:	0.01
付款银行:	OTHERS
支付手续费:	0
支付状态:	SUCCESS
============================================================================

写入时间:	2023-09-15 10:07:23
支付时间:	2023-09-15T10:07:20+08:00
商户订单号:	R20230915100609000017
支付方式:	wechat
微信支付系统订单号:	4200001944202309159886018410
支付类型:	NATIVE
总金额:	0.01
用户支付金额:	0.01
付款银行:	OTHERS
支付手续费:	0
支付状态:	SUCCESS
============================================================================

写入时间:	2023-09-15 10:07:37
支付时间:	2023-09-15T10:07:20+08:00
商户订单号:	R20230915100609000017
支付方式:	wechat
微信支付系统订单号:	4200001944202309159886018410
支付类型:	NATIVE
总金额:	0.01
用户支付金额:	0.01
付款银行:	OTHERS
支付手续费:	0
支付状态:	SUCCESS
============================================================================

显示微信服务器在短短5秒内发送了3次通知。由于我的余额修改业务代码位于异步通知中,因此我的余额连续修改了三次。这导致我的余额瞬间暴涨,我的血压也随之上升。这对用户来说是一个“好事”对于我来说就是要命呐,多余的资金还得自己承担。

为了解决并发请求异步回调的问题,我采取了下面的方法:

/**
     *  商户订单号作为标识同一个请求
     *  查询数据库支付订单状态是否修改为已经支付,是则退出
     *  根据订单号首字母区别是客户下单支付还是充值余额
     *  O:客户下单  R: 客户充值
     *  支付成功异步通知
     */
    public function notifyx()
    {
        $paytype = $this->request->param('paytype');
        $pay = Service::checkNotify($paytype);
        if (!$pay) {
            echo '签名错误';
            return;
        }
        $data = Service::isVersionV3() ? $pay->callback() : $pay->verify();

        try {
            $payamount = $paytype == 'alipay' ? $data['total_amount'] : $data['total_fee'] / 100;
            'wechat' == $paytype ? $pay_method = 0 : $pay_method = 1;
            // 微信支付
            if($pay_method == 0){
                $out_trade_no = $data['resource']['ciphertext']['out_trade_no'];
                // 获取 Redis 实例并选择数据库
                $redis = Cache::store('redis')->handler();
                // 选择数据库
                $redis->select(8);
                // 是否添加锁表
                $addLock = false;
                // 使用 Lua 脚本确保分布式锁的原子性
                //redis.call('SET', key, 1, 'EX', 60 * 60 * 24 * 7) -- 设置为7天的过期时间
                $script = <<<LUA
        local key = KEYS[1]
        local exists = redis.call('EXISTS', key)
        if exists == 0 then
        redis.call('SET', key, 1, 'EX', 60 * 2) 
        end
        return exists
LUA;
                $result = $redis->eval($script, [$out_trade_no], 1);

                if ($result == 1) {
                    //订单号已经存在则锁住;
                    $addLock = true;
                }
                if ($addLock) {
                    return;
                }
                // 同一请求已经处理则退出
                if($this->isThameRequest($out_trade_no)){
                    return json(['code' => 'SUCCESS', 'message' => '成功']);
                }
                $transaction_id = $data['resource']['ciphertext']['transaction_id'];
                $trade_type = $data['resource']['ciphertext']['trade_type'];
                $success_time = $data['resource']['ciphertext']['success_time'];
                $total = $data['resource']['ciphertext']['amount']['total'] * 0.01;
                $payer_total = $data['resource']['ciphertext']['amount']['payer_total'] * 0.01;
                $openid = $data['resource']['ciphertext']['payer']['openid'];
                $bank_type = $data['resource']['ciphertext']['bank_type'];
                $status = $data['resource']['ciphertext']['trade_state'];
                // 记录日志
                $logMessage = "写入时间:\t".date('Y-m-d H:i:s')."\n";
                $logMessage .= "支付时间:\t" . $success_time . "\n";
                $logMessage .= "商户订单号:\t" . $out_trade_no . "\n";
                $logMessage .= "支付方式:\t" . $paytype. "\n";
                $logMessage .= "微信支付系统订单号:\t" . $transaction_id . "\n";
                $logMessage .= "支付类型:\t" . $trade_type. "\n";
                $logMessage .= "总金额:\t" . $total. "\n";
                $logMessage .= "用户支付金额:\t" . $payer_total. "\n";
                $logMessage .= "用户标识:\t" . $openid. "\n";
                $logMessage .= "付款银行:\t" . $bank_type. "\n";
                $logMessage .= "支付手续费:\t" . $payamount. "\n";
                $logMessage .= "支付状态:\t" . $status. "\n";
                $logMessage .= "============================================================================"."\n\n";
                try{
                    // 写入日志文件
                    $this->recodeLog(self::PAYLOG,'wechat.txt',$logMessage);
                }catch (\think\Exception $e){
                    $errlogMessage = "时间:\t" . date('Y-m-d H:i:s') . "\n";
                    $errlogMessage .= "日志内容:\t" . $e . "\n";
                    $this->recodeLog(self::PAYLOG,'wechat_error.txt',$errlogMessage);
                }
                //你可以在此编写订单逻辑
                if($data['resource']['ciphertext']['trade_state'] == 'SUCCESS'){
                    $saveData = [
                        //'id'                =>      $out_trade_no,
                        'pay_method_type'   =>      $pay_method,
                        'transaction_id'    =>      $transaction_id,
                        'trade_type'        =>      $trade_type,
                        'success_time'      =>      $success_time,
                        'total'             =>      $total,
                        'payer_total'       =>      $payer_total,
                        'openid'            =>      $openid,
                        'bank_type'         =>      $bank_type,

                        //'admin_id'          =>      $this->auth->id, // 下单客户
                        'ispay'             =>      1,
                        'status'            =>      2, // 目前订单都默认审核通过
                    ];
                    $rechargeData = [
                        //'admin_id'                           =>      $this->auth->id,
                        //'recharge_order_number'              =>      $out_trade_no,
                        'transaction_serial_number'          =>      $transaction_id,
                        'recharge_amount'                    =>      $total,
                        'recharge_time'                      =>      $success_time,
                        'recharge_channel'                   =>      0, //支付渠道 0:微信,1支付宝
                        'recharge_type'                      =>      0,   //支付渠道 0:微信,1支付宝
                        'ispay'                              =>      1,
                    ];
                    Db::startTrans();
                    try{
                        if($this->getFirstLetter($out_trade_no) == 'O'){
                            // 客户下单支付
                            $isExistOrder = (new \app\admin\model\Orders())->where('id',$out_trade_no)->find();
                            if(!empty($isExistOrder)){
                                (new \app\admin\model\Orders())->where('id',$out_trade_no)->update($saveData);
                            }
                        }else if($this->getFirstLetter($out_trade_no) == 'R'){
                            // 客户充值余额
                            // 查找recharge对应的admin_id 和 recharge_order_number
                            $isExistRecharge = Db::table('fa_recharge')->field('admin_id,recharge_order_number')->where('recharge_order_number', $out_trade_no)->find();
                            if (!empty($isExistRecharge)) {
                                Db::table('fa_recharge')->where('recharge_order_number', $out_trade_no)->update($rechargeData);
                                // 修改余额
                                $account_balance = (new Customer())
                                    ->field('account_balance')
                                    ->where('admin_id',$isExistRecharge['admin_id'])->find();
                                $balance = $account_balance['account_balance'] + $total;
                                (new Customer())
                                    ->where('admin_id',$isExistRecharge['admin_id'])
                                    ->update(['account_balance'=>$balance]);
                            }
                        }
                        Db::commit();
                    }catch (\Exception $exception){
                        $errlogMessage = "时间:\t" . date('Y-m-d H:i:s') . "\n";
                        $errlogMessage .= "数据库事务日志内容:\t" . $exception . "\n";
                        $this->recodeLog(self::PAYLOG,'wechat_error.txt',$errlogMessage);
                        Db::rollback();
                    }
                    return json(['code' => 'SUCCESS', 'message' => '成功']);
                }
            }else {  // 支付宝
                $out_trade_no = $data['out_trade_no'];
                // 获取 Redis 实例并选择数据库
                $redis = Cache::store('redis')->handler();
                // 选择数据库
                $redis->select(8);
                // 是否添加锁表
                $addLock = false;
                // 使用 Lua 脚本确保分布式锁的原子性
                //redis.call('SET', key, 1, 'EX', 60 * 60 * 24 * 7) -- 设置为7天的过期时间
                $script = <<<LUA
        local key = KEYS[1]
        local exists = redis.call('EXISTS', key)
        if exists == 0 then
        redis.call('SET', key, 1, 'EX', 60 * 2) 
        end
        return exists
LUA;
                $result = $redis->eval($script, [$out_trade_no], 1);

                if ($result == 1) {
                    // 订单号已经存在则锁住;
                    $addLock = true;
                }
                if ($addLock) {
                    return;
                }
                // 同一请求已经处理则退出
                if($this->isThameRequest($out_trade_no)){
                    return json('success',200);
                }
                $trade_no = $data['trade_no'];
                $seller_id = $data['seller_id'];
                $success_time = $data['gmt_payment'];
                $total = $data['total_amount'];
                $status = $data['trade_status'];
                // 记录日志
                $logMessage = "写入时间:\t".date('Y-m-d H:i:s')."\n";
                $logMessage .= "支付时间:\t" . $success_time . "\n";
                $logMessage .= "商户订单号:\t" . $out_trade_no . "\n";
                $logMessage .= "支付方式:\t" . $paytype . "\n";
                $logMessage .= "交易金额:\t" . $total . "\n";
                $logMessage .= "支付状态:\t" . $status . "\n";
                $logMessage .= "============================================================================" . "\n\n";
                try {
                    // 写入日志文件
                    $this->recodeLog(self::PAYLOG,'ali.txt',$logMessage);
                } catch (\think\Exception $e) {
                    $errlogMessage = "时间:\t" . date('Y-m-d H:i:s') . "\n";
                    $errlogMessage .= "日志内容:\t" . $e . "\n";
                    $this->recodeLog(self::PAYLOG,'ali.txt',$logMessage);
                }
                //你可以在此编写订单逻辑
                if ($data['trade_status'] == 'TRADE_SUCCESS') {
                    $saveData = [
                        //'id' => $out_trade_no,
                        'pay_method_type' => $pay_method,  // 支付宝支付
                        'trade_no' => $trade_no,
                        'seller_id' => $seller_id,
                        'success_time' => $success_time,
                        'total' => $total,

                        //'admin_id' => $this->auth->id, // 下单客户
                        'ispay' => 1,
                        'status' => 2, // 目前订单都默认审核通过
                    ];
                    $rechargeData = [
                        //'admin_id' => $this->auth->id,
                        //'recharge_order_number' => $out_trade_no,
                        'transaction_serial_number' => $trade_no,
                        'recharge_amount' => $total,
                        'recharge_time' => $success_time,
                        'recharge_channel' => 1, //支付渠道 0:微信,1支付宝
                        'recharge_type' => 1,   //支付渠道 0:微信,1支付宝
                        'ispay'         => 1,
                    ];
                    Db::startTrans();
                    try {
                        if ($this->getFirstLetter($out_trade_no) == 'O') {
                            // 客户下单支付
                            $isExistOrder = (new \app\admin\model\Orders())->where('id', $out_trade_no)->find();
                            if (!empty($isExistOrder)) {
                                (new \app\admin\model\Orders())->where('id',$out_trade_no)->update($saveData);
                            }
                        } else if ($this->getFirstLetter($out_trade_no) == 'R') {
                            // 客户充值
                            // 查找recharge对应的admin_id 和 recharge_order_number
                            $isExistRecharge = Db::table('fa_recharge')->field('admin_id,recharge_order_number')->where('recharge_order_number', $out_trade_no)->find();
                            if (!empty($isExistRecharge)) {
                                Db::table('fa_recharge')->where('recharge_order_number', $isExistRecharge['recharge_order_number'])->update($rechargeData);
                                // 修改余额
                                $account_balance = (new Customer())
                                    ->field('account_balance')
                                    ->where('admin_id',$isExistRecharge['admin_id'])->find();
                                $balance = $account_balance['account_balance'] + $total;
                                (new Customer())
                                    ->where('admin_id',$isExistRecharge['admin_id'])
                                    ->update(['account_balance'=>$balance]);
                            }
                        }
                        Db::commit();
                    } catch (\Exception $exception) {
                        $errlogMessage = "时间:\t" . date('Y-m-d H:i:s') . "\n";
                        $errlogMessage .= "日志内容:\t" . $exception . "\n";
                        //file_put_contents($errorLogFile, $errlogMessage, FILE_APPEND);
                        $this->recodeLog(self::PAYLOG,'ali_error.txt',$errlogMessage);
                        Db::rollback();
                    }
                    return json('success',200);
                }
            }
        } catch (Exception $e) {

        }
        return $pay->success()->send();
    }


/**
     * 根据订单号标识是否是同一请求,同一请求已经处理则返回
     * @param $out_trade_no
     * @return true|void
     */
    public function isThameRequest($out_trade_no){
        if($this->getFirstLetter($out_trade_no) == 'O'){
            // 客户下单支付
            $isExistOrder = (new \app\admin\model\Orders())->field('ispay')->where('id',$out_trade_no)->find();
            if(!empty($isExistOrder) && $isExistOrder['ispay'] == 1){
                return true;
            }
        }else if($this->getFirstLetter($out_trade_no) == 'R'){
            // 客户充值余额
            $isExistRecharge = Db::table('fa_recharge')->field('ispay')->where('recharge_order_number', $out_trade_no)->find();
            if (!empty($isExistRecharge) && $isExistRecharge['ispay'] == 1) {
                return true;
            }
        }
    }

尽管这种方法解决了问题,但我仍然觉得它有点缺陷,我都不知道微信服务器是否接收到我的响应,总之我的数据保持了一致性,那先告一段落吧。在FastAdmin支付插件中的代码直接返回了响应但是我没解决这个问题,不知道是不是有bug。如果您有更好的解决方法,欢迎留言分享,让我们一起探讨!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值