297 行
11 KiB
PHP
297 行
11 KiB
PHP
<?php
|
||
|
||
namespace addons\shopro_huifu\library;
|
||
|
||
use addons\shopro\model\Order;
|
||
use think\Db;
|
||
use think\Log;
|
||
|
||
class HuifuNotifyHandler
|
||
{
|
||
protected $client;
|
||
|
||
public function __construct()
|
||
{
|
||
$this->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,
|
||
];
|
||
}
|
||
}
|