client = new HuifuClient(); } public function handlePayNotify(array $raw) { $sign = HuifuNotifyParser::normalizeSign((string) ($raw['sign'] ?? '')); $respData = $raw['resp_data'] ?? ''; if (is_array($respData)) { Log::error('[shopro_huifu] notify resp_data is array, verify may fail'); $respData = json_encode($respData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); } $respData = (string) $respData; if ($respData === '' || $sign === '') { Log::error('[shopro_huifu] notify missing sign/resp_data'); return $this->ack('', false); } if (!$this->client->verifyNotify($respData, $sign)) { Log::error('[shopro_huifu] notify verify fail, resp_data_len=' . strlen($respData) . ' sign_len=' . strlen($sign)); return $this->ack('', false); } $data = json_decode($respData, true); if (!$data) { Log::error('[shopro_huifu] notify resp_data json decode fail'); return $this->ack('', false); } $reqSeqId = trim((string) ($data['req_seq_id'] ?? '')); $huifuId = trim((string) ($data['huifu_id'] ?? '')); $transStat = trim((string) ($data['trans_stat'] ?? '')); if ($reqSeqId === '') { Log::error('[shopro_huifu] notify missing req_seq_id'); return $this->ack('', false); } Log::info('[shopro_huifu] notify parsed: req_seq_id=' . $reqSeqId . ' trans_stat=' . ($transStat ?: '-') . ' resp_code=' . ($data['resp_code'] ?? '-') . ' acct_stat=' . ($data['acct_stat'] ?? '-')); $order = $this->resolveOrder($reqSeqId); if ($order && $order['status'] > 0) { $this->doneNotify($huifuId, $reqSeqId, 'pay_completed'); return $this->ack($reqSeqId, true); } $paidData = null; if ($transStat === 'S') { $paidData = $data; } else { $paidData = $this->resolvePaidDataViaQuery($reqSeqId, $data); if ($paidData) { Log::info('[shopro_huifu] notify query fallback paid: req_seq_id=' . $reqSeqId); } } if (!$paidData) { HuifuPayService::savePayExt($order ? $order['id'] : 0, $reqSeqId, [ 'trans_stat' => $transStat ?: 'P', 'hf_seq_id' => $data['hf_seq_id'] ?? '', ]); Log::info('[shopro_huifu] notify pending ack: req_seq_id=' . $reqSeqId . ' trans_stat=' . ($transStat ?: 'empty')); return $this->ack($reqSeqId, true); } if (!$this->markNotify($huifuId, $reqSeqId, 'pay_completed', $respData)) { return $this->ack($reqSeqId, true); } if (!$order) { $order = $this->resolveOrder($reqSeqId); } if (!$order) { Log::error('[shopro_huifu] notify order not found: req_seq_id=' . $reqSeqId); return $this->ack($reqSeqId, false); } $payFee = floatval($paidData['trans_amt'] ?? $order['total_fee']); if (abs($payFee - floatval($order['total_fee'])) > 0.001) { Log::error('[shopro_huifu] notify amount mismatch: req_seq_id=' . $reqSeqId . ' notify=' . $payFee . ' order=' . $order['total_fee']); return $this->ack($reqSeqId, false); } $wxFields = HuifuWxNotifyHelper::extract($paidData); $orderSn = $reqSeqId; try { Db::transaction(function () use ($order, $paidData, $payFee, $orderSn, $wxFields) { HuifuPayService::savePayExt($order['id'], $orderSn, [ 'req_date' => $paidData['req_date'] ?? '', 'req_seq_id' => $orderSn, 'hf_seq_id' => $paidData['hf_seq_id'] ?? '', 'trans_stat' => 'S', 'extra' => HuifuWxNotifyHelper::encodeExtra($wxFields), ]); if ($order['order_sn'] !== $orderSn) { Db::name('shopro_order')->where('id', $order['id'])->update([ 'order_sn' => $orderSn, 'pay_type' => 'huifu_wechat', ]); } $notify = [ 'order_sn' => $orderSn, 'transaction_id' => HuifuWxNotifyHelper::resolveTransactionId($wxFields), 'notify_time' => date('Y-m-d H:i:s'), 'buyer_email' => $wxFields['wx_sub_openid'] ?: ($paidData['sub_openid'] ?? ''), 'payment_json' => HuifuWxNotifyHelper::buildPaymentJson($paidData, $wxFields), 'pay_fee' => $payFee, 'pay_type' => 'huifu_wechat', ]; $order->paymentProcess($order, $notify); }); } catch (\Throwable $e) { $msg = trim($e->getMessage()); if ($e instanceof \think\exception\HttpResponseException) { $resp = $e->getResponse(); $msg = 'HttpResponseException: ' . json_encode($resp ? $resp->getData() : null, JSON_UNESCAPED_UNICODE); } elseif ($msg === '') { $msg = get_class($e) . ' @ ' . basename($e->getFile()) . ':' . $e->getLine(); } Log::error('[shopro_huifu] notify process fail: ' . $msg); return $this->ack($reqSeqId, false); } $this->doneNotify($huifuId, $reqSeqId, 'pay_completed'); Log::info('[shopro_huifu] notify paid ok: order_id=' . $order['id'] . ' req_seq_id=' . $reqSeqId); return $this->ack($reqSeqId, true); } /** * 账务/通道通知可能不含 trans_stat=S,以查单二次确认(shared-async-notify) */ protected function resolvePaidDataViaQuery($reqSeqId, array $notifyData) { if ($this->shouldQueryForPaid($notifyData)) { try { $query = (new HuifuQueryService())->queryByOrderSn($reqSeqId); if (($query['trans_stat'] ?? '') === 'S') { return $query; } Log::info('[shopro_huifu] notify query not paid: req_seq_id=' . $reqSeqId . ' trans_stat=' . ($query['trans_stat'] ?? '')); } catch (\Throwable $e) { Log::error('[shopro_huifu] notify query fallback fail: ' . $e->getMessage()); } } return null; } protected function shouldQueryForPaid(array $data) { $transStat = trim((string) ($data['trans_stat'] ?? '')); if ($transStat === 'F') { return false; } if ($transStat === 'S') { return false; } $respCode = trim((string) ($data['resp_code'] ?? '')); if (in_array($respCode, ['00000000', '00000100'], true)) { return true; } $bankMsg = (string) ($data['bank_message'] ?? $data['bank_desc'] ?? ''); if ($bankMsg !== '' && strpos($bankMsg, '成功') !== false) { return true; } if (($data['acct_stat'] ?? '') === 'I') { return true; } return false; } public function resolveOrder($reqSeqId) { $reqSeqId = trim((string) $reqSeqId); if ($reqSeqId === '') { return null; } $order = Order::where('order_sn', $reqSeqId)->find(); if ($order) { return $order; } $ext = Db::name('shopro_huifu_pay_ext')->where('req_seq_id', $reqSeqId)->find(); if (!$ext) { $ext = Db::name('shopro_huifu_pay_ext')->where('order_sn', $reqSeqId)->find(); } if ($ext && !empty($ext['order_id'])) { return Order::where('id', $ext['order_id'])->find(); } return null; } public function applyPaidOrder($order, array $data, $orderSn = '') { $orderSn = $orderSn ?: trim((string) ($data['req_seq_id'] ?? $order['order_sn'])); $payFee = floatval($data['trans_amt'] ?? $order['total_fee']); $wxFields = HuifuWxNotifyHelper::extract($data); Db::transaction(function () use ($order, $data, $payFee, $orderSn, $wxFields) { HuifuPayService::savePayExt($order['id'], $orderSn, [ 'req_date' => $data['req_date'] ?? '', 'req_seq_id' => $orderSn, 'hf_seq_id' => $data['hf_seq_id'] ?? '', 'trans_stat' => 'S', 'extra' => HuifuWxNotifyHelper::encodeExtra($wxFields), ]); if ($order['order_sn'] !== $orderSn) { Db::name('shopro_order')->where('id', $order['id'])->update([ 'order_sn' => $orderSn, 'pay_type' => 'huifu_wechat', ]); } $notify = [ 'order_sn' => $orderSn, 'transaction_id' => HuifuWxNotifyHelper::resolveTransactionId($wxFields), 'notify_time' => date('Y-m-d H:i:s'), 'buyer_email' => $wxFields['wx_sub_openid'] ?? '', 'payment_json' => HuifuWxNotifyHelper::buildPaymentJson($data, $wxFields), 'pay_fee' => $payFee, 'pay_type' => 'huifu_wechat', ]; $order->paymentProcess($order, $notify); }); } protected function markNotify($huifuId, $reqSeqId, $eventType, $payload) { try { Db::name('shopro_huifu_notify_log')->insert([ 'huifu_id' => $huifuId, 'req_seq_id' => $reqSeqId, 'event_type' => $eventType, 'payload' => $payload, 'status' => 0, 'createtime' => time(), ]); return true; } catch (\Exception $e) { $exists = Db::name('shopro_huifu_notify_log') ->where(['huifu_id' => $huifuId, 'req_seq_id' => $reqSeqId, 'event_type' => $eventType]) ->find(); if ($exists && intval($exists['status']) === 1) { return false; } // status=0 表示上次处理失败,允许重试 return true; } } protected function doneNotify($huifuId, $reqSeqId, $eventType) { Db::name('shopro_huifu_notify_log') ->where(['huifu_id' => $huifuId, 'req_seq_id' => $reqSeqId, 'event_type' => $eventType]) ->update(['status' => 1]); } protected function ack($reqSeqId, $success) { return [ 'success' => $success, 'body' => $reqSeqId ? 'RECV_ORD_ID_' . $reqSeqId : '', 'httpCode' => $success ? 200 : 500, ]; } }