当前位置:   article > 正文

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

fastadmin异步处理

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

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

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

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

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

  1. 写入时间: 2023-09-15 10:07:21
  2. 支付时间: 2023-09-15T10:07:20+08:00
  3. 商户订单号: R20230915100609000017
  4. 支付方式: wechat
  5. 微信支付系统订单号: 4200001944202309159886018410
  6. 支付类型: NATIVE
  7. 总金额: 0.01
  8. 用户支付金额: 0.01
  9. 付款银行: OTHERS
  10. 支付手续费: 0
  11. 支付状态: SUCCESS
  12. ============================================================================
  13. 写入时间: 2023-09-15 10:07:23
  14. 支付时间: 2023-09-15T10:07:20+08:00
  15. 商户订单号: R20230915100609000017
  16. 支付方式: wechat
  17. 微信支付系统订单号: 4200001944202309159886018410
  18. 支付类型: NATIVE
  19. 总金额: 0.01
  20. 用户支付金额: 0.01
  21. 付款银行: OTHERS
  22. 支付手续费: 0
  23. 支付状态: SUCCESS
  24. ============================================================================
  25. 写入时间: 2023-09-15 10:07:37
  26. 支付时间: 2023-09-15T10:07:20+08:00
  27. 商户订单号: R20230915100609000017
  28. 支付方式: wechat
  29. 微信支付系统订单号: 4200001944202309159886018410
  30. 支付类型: NATIVE
  31. 总金额: 0.01
  32. 用户支付金额: 0.01
  33. 付款银行: OTHERS
  34. 支付手续费: 0
  35. 支付状态: SUCCESS
  36. ============================================================================

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

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

  1. /**
  2. * 商户订单号作为标识同一个请求
  3. * 查询数据库支付订单状态是否修改为已经支付,是则退出
  4. * 根据订单号首字母区别是客户下单支付还是充值余额
  5. * O:客户下单 R: 客户充值
  6. * 支付成功异步通知
  7. */
  8. public function notifyx()
  9. {
  10. $paytype = $this->request->param('paytype');
  11. $pay = Service::checkNotify($paytype);
  12. if (!$pay) {
  13. echo '签名错误';
  14. return;
  15. }
  16. $data = Service::isVersionV3() ? $pay->callback() : $pay->verify();
  17. try {
  18. $payamount = $paytype == 'alipay' ? $data['total_amount'] : $data['total_fee'] / 100;
  19. 'wechat' == $paytype ? $pay_method = 0 : $pay_method = 1;
  20. // 微信支付
  21. if($pay_method == 0){
  22. $out_trade_no = $data['resource']['ciphertext']['out_trade_no'];
  23. // 获取 Redis 实例并选择数据库
  24. $redis = Cache::store('redis')->handler();
  25. // 选择数据库
  26. $redis->select(8);
  27. // 是否添加锁表
  28. $addLock = false;
  29. // 使用 Lua 脚本确保分布式锁的原子性
  30. //redis.call('SET', key, 1, 'EX', 60 * 60 * 24 * 7) -- 设置为7天的过期时间
  31. $script = <<<LUA
  32. local key = KEYS[1]
  33. local exists = redis.call('EXISTS', key)
  34. if exists == 0 then
  35. redis.call('SET', key, 1, 'EX', 60 * 2)
  36. end
  37. return exists
  38. LUA;
  39. $result = $redis->eval($script, [$out_trade_no], 1);
  40. if ($result == 1) {
  41. //订单号已经存在则锁住;
  42. $addLock = true;
  43. }
  44. if ($addLock) {
  45. return;
  46. }
  47. // 同一请求已经处理则退出
  48. if($this->isThameRequest($out_trade_no)){
  49. return json(['code' => 'SUCCESS', 'message' => '成功']);
  50. }
  51. $transaction_id = $data['resource']['ciphertext']['transaction_id'];
  52. $trade_type = $data['resource']['ciphertext']['trade_type'];
  53. $success_time = $data['resource']['ciphertext']['success_time'];
  54. $total = $data['resource']['ciphertext']['amount']['total'] * 0.01;
  55. $payer_total = $data['resource']['ciphertext']['amount']['payer_total'] * 0.01;
  56. $openid = $data['resource']['ciphertext']['payer']['openid'];
  57. $bank_type = $data['resource']['ciphertext']['bank_type'];
  58. $status = $data['resource']['ciphertext']['trade_state'];
  59. // 记录日志
  60. $logMessage = "写入时间:\t".date('Y-m-d H:i:s')."\n";
  61. $logMessage .= "支付时间:\t" . $success_time . "\n";
  62. $logMessage .= "商户订单号:\t" . $out_trade_no . "\n";
  63. $logMessage .= "支付方式:\t" . $paytype. "\n";
  64. $logMessage .= "微信支付系统订单号:\t" . $transaction_id . "\n";
  65. $logMessage .= "支付类型:\t" . $trade_type. "\n";
  66. $logMessage .= "总金额:\t" . $total. "\n";
  67. $logMessage .= "用户支付金额:\t" . $payer_total. "\n";
  68. $logMessage .= "用户标识:\t" . $openid. "\n";
  69. $logMessage .= "付款银行:\t" . $bank_type. "\n";
  70. $logMessage .= "支付手续费:\t" . $payamount. "\n";
  71. $logMessage .= "支付状态:\t" . $status. "\n";
  72. $logMessage .= "============================================================================"."\n\n";
  73. try{
  74. // 写入日志文件
  75. $this->recodeLog(self::PAYLOG,'wechat.txt',$logMessage);
  76. }catch (\think\Exception $e){
  77. $errlogMessage = "时间:\t" . date('Y-m-d H:i:s') . "\n";
  78. $errlogMessage .= "日志内容:\t" . $e . "\n";
  79. $this->recodeLog(self::PAYLOG,'wechat_error.txt',$errlogMessage);
  80. }
  81. //你可以在此编写订单逻辑
  82. if($data['resource']['ciphertext']['trade_state'] == 'SUCCESS'){
  83. $saveData = [
  84. //'id' => $out_trade_no,
  85. 'pay_method_type' => $pay_method,
  86. 'transaction_id' => $transaction_id,
  87. 'trade_type' => $trade_type,
  88. 'success_time' => $success_time,
  89. 'total' => $total,
  90. 'payer_total' => $payer_total,
  91. 'openid' => $openid,
  92. 'bank_type' => $bank_type,
  93. //'admin_id' => $this->auth->id, // 下单客户
  94. 'ispay' => 1,
  95. 'status' => 2, // 目前订单都默认审核通过
  96. ];
  97. $rechargeData = [
  98. //'admin_id' => $this->auth->id,
  99. //'recharge_order_number' => $out_trade_no,
  100. 'transaction_serial_number' => $transaction_id,
  101. 'recharge_amount' => $total,
  102. 'recharge_time' => $success_time,
  103. 'recharge_channel' => 0, //支付渠道 0:微信,1支付宝
  104. 'recharge_type' => 0, //支付渠道 0:微信,1支付宝
  105. 'ispay' => 1,
  106. ];
  107. Db::startTrans();
  108. try{
  109. if($this->getFirstLetter($out_trade_no) == 'O'){
  110. // 客户下单支付
  111. $isExistOrder = (new \app\admin\model\Orders())->where('id',$out_trade_no)->find();
  112. if(!empty($isExistOrder)){
  113. (new \app\admin\model\Orders())->where('id',$out_trade_no)->update($saveData);
  114. }
  115. }else if($this->getFirstLetter($out_trade_no) == 'R'){
  116. // 客户充值余额
  117. // 查找recharge对应的admin_id 和 recharge_order_number
  118. $isExistRecharge = Db::table('fa_recharge')->field('admin_id,recharge_order_number')->where('recharge_order_number', $out_trade_no)->find();
  119. if (!empty($isExistRecharge)) {
  120. Db::table('fa_recharge')->where('recharge_order_number', $out_trade_no)->update($rechargeData);
  121. // 修改余额
  122. $account_balance = (new Customer())
  123. ->field('account_balance')
  124. ->where('admin_id',$isExistRecharge['admin_id'])->find();
  125. $balance = $account_balance['account_balance'] + $total;
  126. (new Customer())
  127. ->where('admin_id',$isExistRecharge['admin_id'])
  128. ->update(['account_balance'=>$balance]);
  129. }
  130. }
  131. Db::commit();
  132. }catch (\Exception $exception){
  133. $errlogMessage = "时间:\t" . date('Y-m-d H:i:s') . "\n";
  134. $errlogMessage .= "数据库事务日志内容:\t" . $exception . "\n";
  135. $this->recodeLog(self::PAYLOG,'wechat_error.txt',$errlogMessage);
  136. Db::rollback();
  137. }
  138. return json(['code' => 'SUCCESS', 'message' => '成功']);
  139. }
  140. }else { // 支付宝
  141. $out_trade_no = $data['out_trade_no'];
  142. // 获取 Redis 实例并选择数据库
  143. $redis = Cache::store('redis')->handler();
  144. // 选择数据库
  145. $redis->select(8);
  146. // 是否添加锁表
  147. $addLock = false;
  148. // 使用 Lua 脚本确保分布式锁的原子性
  149. //redis.call('SET', key, 1, 'EX', 60 * 60 * 24 * 7) -- 设置为7天的过期时间
  150. $script = <<<LUA
  151. local key = KEYS[1]
  152. local exists = redis.call('EXISTS', key)
  153. if exists == 0 then
  154. redis.call('SET', key, 1, 'EX', 60 * 2)
  155. end
  156. return exists
  157. LUA;
  158. $result = $redis->eval($script, [$out_trade_no], 1);
  159. if ($result == 1) {
  160. // 订单号已经存在则锁住;
  161. $addLock = true;
  162. }
  163. if ($addLock) {
  164. return;
  165. }
  166. // 同一请求已经处理则退出
  167. if($this->isThameRequest($out_trade_no)){
  168. return json('success',200);
  169. }
  170. $trade_no = $data['trade_no'];
  171. $seller_id = $data['seller_id'];
  172. $success_time = $data['gmt_payment'];
  173. $total = $data['total_amount'];
  174. $status = $data['trade_status'];
  175. // 记录日志
  176. $logMessage = "写入时间:\t".date('Y-m-d H:i:s')."\n";
  177. $logMessage .= "支付时间:\t" . $success_time . "\n";
  178. $logMessage .= "商户订单号:\t" . $out_trade_no . "\n";
  179. $logMessage .= "支付方式:\t" . $paytype . "\n";
  180. $logMessage .= "交易金额:\t" . $total . "\n";
  181. $logMessage .= "支付状态:\t" . $status . "\n";
  182. $logMessage .= "============================================================================" . "\n\n";
  183. try {
  184. // 写入日志文件
  185. $this->recodeLog(self::PAYLOG,'ali.txt',$logMessage);
  186. } catch (\think\Exception $e) {
  187. $errlogMessage = "时间:\t" . date('Y-m-d H:i:s') . "\n";
  188. $errlogMessage .= "日志内容:\t" . $e . "\n";
  189. $this->recodeLog(self::PAYLOG,'ali.txt',$logMessage);
  190. }
  191. //你可以在此编写订单逻辑
  192. if ($data['trade_status'] == 'TRADE_SUCCESS') {
  193. $saveData = [
  194. //'id' => $out_trade_no,
  195. 'pay_method_type' => $pay_method, // 支付宝支付
  196. 'trade_no' => $trade_no,
  197. 'seller_id' => $seller_id,
  198. 'success_time' => $success_time,
  199. 'total' => $total,
  200. //'admin_id' => $this->auth->id, // 下单客户
  201. 'ispay' => 1,
  202. 'status' => 2, // 目前订单都默认审核通过
  203. ];
  204. $rechargeData = [
  205. //'admin_id' => $this->auth->id,
  206. //'recharge_order_number' => $out_trade_no,
  207. 'transaction_serial_number' => $trade_no,
  208. 'recharge_amount' => $total,
  209. 'recharge_time' => $success_time,
  210. 'recharge_channel' => 1, //支付渠道 0:微信,1支付宝
  211. 'recharge_type' => 1, //支付渠道 0:微信,1支付宝
  212. 'ispay' => 1,
  213. ];
  214. Db::startTrans();
  215. try {
  216. if ($this->getFirstLetter($out_trade_no) == 'O') {
  217. // 客户下单支付
  218. $isExistOrder = (new \app\admin\model\Orders())->where('id', $out_trade_no)->find();
  219. if (!empty($isExistOrder)) {
  220. (new \app\admin\model\Orders())->where('id',$out_trade_no)->update($saveData);
  221. }
  222. } else if ($this->getFirstLetter($out_trade_no) == 'R') {
  223. // 客户充值
  224. // 查找recharge对应的admin_id 和 recharge_order_number
  225. $isExistRecharge = Db::table('fa_recharge')->field('admin_id,recharge_order_number')->where('recharge_order_number', $out_trade_no)->find();
  226. if (!empty($isExistRecharge)) {
  227. Db::table('fa_recharge')->where('recharge_order_number', $isExistRecharge['recharge_order_number'])->update($rechargeData);
  228. // 修改余额
  229. $account_balance = (new Customer())
  230. ->field('account_balance')
  231. ->where('admin_id',$isExistRecharge['admin_id'])->find();
  232. $balance = $account_balance['account_balance'] + $total;
  233. (new Customer())
  234. ->where('admin_id',$isExistRecharge['admin_id'])
  235. ->update(['account_balance'=>$balance]);
  236. }
  237. }
  238. Db::commit();
  239. } catch (\Exception $exception) {
  240. $errlogMessage = "时间:\t" . date('Y-m-d H:i:s') . "\n";
  241. $errlogMessage .= "日志内容:\t" . $exception . "\n";
  242. //file_put_contents($errorLogFile, $errlogMessage, FILE_APPEND);
  243. $this->recodeLog(self::PAYLOG,'ali_error.txt',$errlogMessage);
  244. Db::rollback();
  245. }
  246. return json('success',200);
  247. }
  248. }
  249. } catch (Exception $e) {
  250. }
  251. return $pay->success()->send();
  252. }
  253. /**
  254. * 根据订单号标识是否是同一请求,同一请求已经处理则返回
  255. * @param $out_trade_no
  256. * @return true|void
  257. */
  258. public function isThameRequest($out_trade_no){
  259. if($this->getFirstLetter($out_trade_no) == 'O'){
  260. // 客户下单支付
  261. $isExistOrder = (new \app\admin\model\Orders())->field('ispay')->where('id',$out_trade_no)->find();
  262. if(!empty($isExistOrder) && $isExistOrder['ispay'] == 1){
  263. return true;
  264. }
  265. }else if($this->getFirstLetter($out_trade_no) == 'R'){
  266. // 客户充值余额
  267. $isExistRecharge = Db::table('fa_recharge')->field('ispay')->where('recharge_order_number', $out_trade_no)->find();
  268. if (!empty($isExistRecharge) && $isExistRecharge['ispay'] == 1) {
  269. return true;
  270. }
  271. }
  272. }

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

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/知新_RL/article/detail/132074
推荐阅读
相关标签
  

闽ICP备14008679号