Recently, FastAdmin’s payment plug-in was used to develop a PC-side function for scanning QR codes to recharge the balance. After successfully recharging, I encountered the problem that the WeChat server sent asynchronous callback notifications multiple times and my data was modified multiple times. I responded according to the callback in the FastAdmin payment plug-in,
//The following sentence must be executed, and there cannot be any output before this return $pay->success()->send();
However, I feel that this line of code has no practical effect (I don’t know if it is a bug, and I have not found any relevant solutions online), so I did not send a successful response message to the WeChat server to stop WeChat from pushing. This caused the WeChat server to continue sending notifications, which eventually caused my data to be modified multiple times. The following content mainly solves this problem.
I checked the official WeChat payment documentation and found that the document clearly stated: “The same notification may be sent to the merchant system multiple times. The merchant system must be able to handle duplicate notifications correctly.” The recommended approach is that when the merchant system receives the notification, First check the status of relevant business data to determine whether the notification has been processed. If it has not been processed yet, it will be processed; if it has been processed, the result of successful processing will be returned directly. Before processing business data, data locks must be used for concurrency control to prevent data chaos caused by function reentry.
Concurrency control is indeed something to consider. Below is the log I printed:
Writing time: 2023-09-15 10:07:21 Payment time: 2023-09-15T10:07:20 + 08:00 Merchant order number: R20230915100609000017 Payment method: wechat WeChat payment system order number: 4200001944202309159886018410 Payment type: NATIVE Total amount: 0.01 User payment amount: 0.01 Paying Bank: OTHERS Payment fee: 0 Payment status: SUCCESS ================================================== ========================== Writing time: 2023-09-15 10:07:23 Payment time: 2023-09-15T10:07:20 + 08:00 Merchant order number: R20230915100609000017 Payment method: wechat WeChat payment system order number: 4200001944202309159886018410 Payment type: NATIVE Total amount: 0.01 User payment amount: 0.01 Paying Bank: OTHERS Payment fee: 0 Payment status: SUCCESS ================================================== ========================== Writing time: 2023-09-15 10:07:37 Payment time: 2023-09-15T10:07:20 + 08:00 Merchant order number: R20230915100609000017 Payment method: wechat WeChat payment system order number: 4200001944202309159886018410 Payment type: NATIVE Total amount: 0.01 User payment amount: 0.01 Paying Bank: OTHERS Payment fee: 0 Payment status: SUCCESS ================================================== ==========================
It shows that the WeChat server sent 3 notifications in just 5 seconds. Since my balance modification business code is in an asynchronous notification, my balance is modified three times in a row. This caused my balance to skyrocket in an instant, and my blood pressure to rise with it. This is a “good thing” for users, but it is life-threatening for me. I have to bear the extra funds myself.
In order to solve the problem of concurrent requests for asynchronous callbacks, I took the following approach:
/** * Merchant order number is used to identify the same request * Query the database payment order status to see if it has been changed to paid, if so, exit. * According to the first letter of the order number, it is distinguished whether the customer places an order to pay or recharges the balance. * O: Customer orders R: Customer recharges * Asynchronous notification of successful payment */ public function notifyx() { $paytype = $this->request->param('paytype'); $pay = Service::checkNotify($paytype); if (!$pay) { echo 'Signature error'; 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; // WeChat Pay if($pay_method == 0){ $out_trade_no = $data['resource']['ciphertext']['out_trade_no']; // Get the Redis instance and select the database $redis = Cache::store('redis')->handler(); //Select database $redis->select(8); // Whether to add a lock table $addLock = false; // Use Lua script to ensure the atomicity of distributed locks //redis.call('SET', key, 1, 'EX', 60 * 60 * 24 * 7) -- Set the expiration time to 7 days $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) { //If the order number already exists, lock it; $addLock = true; } if ($addLock) { return; } //Exit if the same request has been processed if($this->isThameRequest($out_trade_no)){ return json(['code' => 'SUCCESS', 'message' => 'Success']); } $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']; // record log $logMessage = "Writing time:\t".date('Y-m-d H:i:s')."\\ "; $logMessage .= "Payment time:\t" . $success_time . "\\ "; $logMessage .= "Merchant order number:\t" . $out_trade_no . "\\ "; $logMessage .= "Payment method:\t" . $paytype. "\\ "; $logMessage .= "WeChat payment system order number:\t" . $transaction_id . "\\ "; $logMessage .= "Payment type:\t" . $trade_type. "\\ "; $logMessage .= "Total amount:\t" . $total. "\\ "; $logMessage .= "User payment amount:\t" . $payer_total. "\\ "; $logMessage .= "User ID:\t" . $openid. "\\ "; $logMessage .= "Payment bank:\t" . $bank_type. "\\ "; $logMessage .= "Payment fee:\t" . $payamount. "\\ "; $logMessage .= "Payment Status:\t" . $status. "\\ "; $logMessage .= "============================================ ================================"."\\ \\ "; try{ //Write to log file $this->recodeLog(self::PAYLOG,'wechat.txt',$logMessage); }catch (\think\Exception $e){ $errlogMessage = "Time:\t" . date('Y-m-d H:i:s') . "\\ "; $errlogMessage .= "Log content:\t" . $e . "\\ "; $this->recodeLog(self::PAYLOG,'wechat_error.txt',$errlogMessage); } //You can write order logic here 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, // Customer who placed the order 'ispay' => 1, 'status' => 2, // Currently all orders are approved by default ]; $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, //Payment channel 0: WeChat, 1 Alipay 'recharge_type' => 0, //Payment channel 0: WeChat, 1 Alipay 'ispay' => 1, ]; Db::startTrans(); try{ if($this->getFirstLetter($out_trade_no) == 'O'){ // Customer places order and pays $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'){ //Customer recharge balance // Find the admin_id and recharge_order_number corresponding to recharge $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); //Modify balance $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 = "Time:\t" . date('Y-m-d H:i:s') . "\\ "; $errlogMessage .= "Database transaction log content:\t" . $exception . "\\ "; $this->recodeLog(self::PAYLOG,'wechat_error.txt',$errlogMessage); Db::rollback(); } return json(['code' => 'SUCCESS', 'message' => 'Success']); } }else { // Alipay $out_trade_no = $data['out_trade_no']; // Get the Redis instance and select the database $redis = Cache::store('redis')->handler(); //Select database $redis->select(8); // Whether to add a lock table $addLock = false; // Use Lua script to ensure the atomicity of distributed locks //redis.call('SET', key, 1, 'EX', 60 * 60 * 24 * 7) -- Set the expiration time to 7 days $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) { // If the order number already exists, lock it; $addLock = true; } if ($addLock) { return; } //Exit if the same request has been processed 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']; // record log $logMessage = "Writing time:\t".date('Y-m-d H:i:s')."\\ "; $logMessage .= "Payment time:\t" . $success_time . "\\ "; $logMessage .= "Merchant order number:\t" . $out_trade_no . "\\ "; $logMessage .= "Payment method:\t" . $paytype . "\\ "; $logMessage .= "Transaction amount:\t" . $total . "\\ "; $logMessage .= "Payment Status:\t" . $status . "\\ "; $logMessage .= "============================================ ================================" . "\\ \\ "; try { //Write to log file $this->recodeLog(self::PAYLOG,'ali.txt',$logMessage); } catch (\think\Exception $e) { $errlogMessage = "Time:\t" . date('Y-m-d H:i:s') . "\\ "; $errlogMessage .= "Log content:\t" . $e . "\\ "; $this->recodeLog(self::PAYLOG,'ali.txt',$logMessage); } //You can write order logic here if ($data['trade_status'] == 'TRADE_SUCCESS') { $saveData = [ //'id' => $out_trade_no, 'pay_method_type' => $pay_method, // Alipay payment 'trade_no' => $trade_no, 'seller_id' => $seller_id, 'success_time' => $success_time, 'total' => $total, //'admin_id' => $this->auth->id, // Customer who placed the order 'ispay' => 1, 'status' => 2, // Currently all orders are approved by default ]; $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, //Payment channel 0: WeChat, 1 Alipay 'recharge_type' => 1, //Payment channel 0: WeChat, 1 Alipay 'ispay' => 1, ]; Db::startTrans(); try { if ($this->getFirstLetter($out_trade_no) == 'O') { // Customer places order and pays $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') { //Customer recharge // Find the admin_id and recharge_order_number corresponding to recharge $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); //Modify balance $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 = "Time:\t" . date('Y-m-d H:i:s') . "\\ "; $errlogMessage .= "Log content:\t" . $exception . "\\ "; //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(); } /** * Identifies whether it is the same request based on the order number. If the same request has been processed, it will be returned. * @param $out_trade_no * @return true|void */ public function isThameRequest($out_trade_no){ if($this->getFirstLetter($out_trade_no) == 'O'){ // Customer places order and pays $isExistOrder = (new \app\admin\model\Orders())->field('ispay')->where('id',$out_trade_no)->find(); if(!empty($isExistOrder) & amp; & amp; $isExistOrder['ispay'] == 1){ return true; } }else if($this->getFirstLetter($out_trade_no) == 'R'){ //Customer recharge balance $isExistRecharge = Db::table('fa_recharge')->field('ispay')->where('recharge_order_number', $out_trade_no)->find(); if (!empty($isExistRecharge) & amp; & amp; $isExistRecharge['ispay'] == 1) { return true; } } }
Although this method solved the problem, I still feel that it is a bit flawed. I don’t even know whether the WeChat server received my response. In short, my data maintains consistency, so let’s call it a day. The code in the FastAdmin payment plug-in directly returns the response, but I haven’t solved this problem and I don’t know if there is a bug. If you have a better solution, please leave a message to share and let us discuss it together!