赞
踩
最近使用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。如果您有更好的解决方法,欢迎留言分享,让我们一起探讨!
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。