最近使用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。如果您有更好的解决方法,欢迎留言分享,让我们一起探讨!