diff --git a/addons/shopro/library/traits/model/order/OrderOper.php b/addons/shopro/library/traits/model/order/OrderOper.php index 32250dc..59875bb 100644 --- a/addons/shopro/library/traits/model/order/OrderOper.php +++ b/addons/shopro/library/traits/model/order/OrderOper.php @@ -1025,6 +1025,28 @@ trait OrderOper // 退款完成 $item->refund_status = \app\admin\model\shopro\order\OrderItem::REFUND_STATUS_FINISH; // 退款完成 + + // 自动联动对齐该商品项关联的待处理/售后中售后单状态 + $aftersales = \addons\shopro\model\OrderAftersale::where('order_item_id', $item->id) + ->where('aftersale_status', 'in', [ + \addons\shopro\model\OrderAftersale::AFTERSALE_STATUS_NOOPER, + \addons\shopro\model\OrderAftersale::AFTERSALE_STATUS_AFTERING + ])->select(); + foreach ($aftersales as $aftersale) { + $aftersale->aftersale_status = \addons\shopro\model\OrderAftersale::AFTERSALE_STATUS_OK; + $aftersale->refund_status = \addons\shopro\model\OrderAftersale::REFUND_STATUS_FINISH; + $aftersale->refund_fee = $item->refund_fee ?: $refundLog->refund_fee; + $aftersale->save(); + + $item->aftersale_status = \addons\shopro\model\OrderAftersale::AFTERSALE_STATUS_OK; + + \addons\shopro\model\OrderAftersaleLog::operAdd($order, $aftersale, null, 'admin', [ + 'reason' => '退款完成自动对齐', + 'content' => '退款成功,系统自动将售后单对齐为完成状态', + 'images' => [] + ]); + } + $item->save(); \addons\shopro\model\OrderAction::operAdd($order, $item, null, 'admin', '退款成功'); } diff --git a/addons/shopro_huifu/library/HuifuRefundService.php b/addons/shopro_huifu/library/HuifuRefundService.php index e5dfece..ea02f1c 100644 --- a/addons/shopro_huifu/library/HuifuRefundService.php +++ b/addons/shopro_huifu/library/HuifuRefundService.php @@ -24,20 +24,25 @@ class HuifuRefundService } $splitOrder = SplitOrder::where('order_id', $order['id'])->where('channel', 'huifu')->where('status', 2)->find(); - if ($splitOrder && !empty($splitOrder['confirm_hf_seq_id'])) { - return $this->confirmRefund($ext, $splitOrder, $refundLog, $refundMoney, $remark); - } try { + if ($splitOrder && !empty($splitOrder['confirm_hf_seq_id'])) { + return $this->confirmRefund($ext, $splitOrder, $refundLog, $refundMoney, $remark); + } return $this->scanpayRefund($ext, $refundLog, $refundMoney, $remark); } catch (\Throwable $e) { - // 如果汇付报错 23000003(申请退款金额大于可退款余额),核查是否此前已成功退款(幂等对齐防多退) + // 核查是否此前已在汇付成功退款(网关状态幂等自动对齐,防多退、防报错卡死) + if ($this->isAlreadyRefunded($ext, $refundMoney)) { + \think\Log::info('[shopro_huifu] 汇付检测到该笔退款此前已成功执行,执行本地状态自动对齐: order_id=' . $order['id']); + $refundLog->payment_json = json_encode([ + 'idempotent' => true, + 'remark' => '汇付核验已退款成功,自动对齐本地状态', + 'raw_error' => $e->getMessage(), + ], JSON_UNESCAPED_UNICODE); + $refundLog->save(); + return true; + } + if (strpos($e->getMessage(), '23000003') !== false || strpos($e->getMessage(), '申请退款金额大于可退款余额') !== false) { - if ($this->isAlreadyRefunded($ext, $refundMoney)) { - \think\Log::info('[shopro_huifu] 汇付检测到该笔退款此前已成功执行,执行本地状态幂等对齐: order_id=' . $order['id']); - $refundLog->payment_json = json_encode(['idempotent' => true, 'remark' => '汇付核验已退款成功,自动对齐本地状态']); - $refundLog->save(); - return true; - } throw new \RuntimeException('汇付商户账户可用余额不足,请在汇付平台充值或留存可用资金后再退款'); } throw $e; @@ -45,15 +50,30 @@ class HuifuRefundService } public function isAlreadyRefunded($ext, $refundMoney) + { + $dates = array_filter(array_unique([ + date('Ymd'), + !empty($ext['req_date']) ? $ext['req_date'] : null, + ])); + + foreach ($dates as $date) { + if ($this->checkAcctLogForRefund($date, $refundMoney)) { + return true; + } + } + return false; + } + + protected function checkAcctLogForRefund($acctDate, $refundMoney) { try { SdkLoader::requireRequest('V2TradeAcctpaymentAcctlogQueryRequest.php'); $request = new \BsPaySdk\request\V2TradeAcctpaymentAcctlogQueryRequest(); $request->setReqSeqId(date('YmdHis') . mt_rand(1000, 9999)); $request->setHuifuId($this->config->get('huifu_id')); - $request->setAcctDate($ext['req_date'] ?? date('Ymd')); + $request->setAcctDate($acctDate); $request->setExtendInfo([ - 'page_size' => '20', + 'page_size' => '50', 'page_num' => '1', 'acct_type' => '01', ]); @@ -75,7 +95,7 @@ class HuifuRefundService } } } catch (\Throwable $ex) { - \think\Log::error('[shopro_huifu] 幂等核查汇付退款流水异常: ' . $ex->getMessage()); + \think\Log::error('[shopro_huifu] 幂等核查汇付退款流水异常(' . $acctDate . '): ' . $ex->getMessage()); } return false; } diff --git a/application/admin/controller/shopro/order/Aftersale.php b/application/admin/controller/shopro/order/Aftersale.php index 9bec17b..945edcf 100644 --- a/application/admin/controller/shopro/order/Aftersale.php +++ b/application/admin/controller/shopro/order/Aftersale.php @@ -283,11 +283,25 @@ class Aftersale extends Backend } $orderItem = OrderItem::where('id', $aftersale['order_item_id'])->lock(true)->find(); - if (!$orderItem || in_array($orderItem['refund_status'], [ + if (!$orderItem) { + throw new \Exception('订单商品不存在'); + } + if (in_array($orderItem['refund_status'], [ \app\admin\model\shopro\order\OrderItem::REFUND_STATUS_OK, \app\admin\model\shopro\order\OrderItem::REFUND_STATUS_FINISH, ])) { - throw new \Exception('订单商品已退款,不能重复退款'); + // 订单商品此前已完成退款,自动对齐售后单状态,防重复扣款与卡死 + $aftersale->aftersale_status = \app\admin\model\shopro\order\Aftersale::AFTERSALE_STATUS_OK; + $aftersale->refund_status = \app\admin\model\shopro\order\Aftersale::REFUND_STATUS_FINISH; + $aftersale->refund_fee = $orderItem->refund_fee ?: $refund_money; + $aftersale->save(); + + OrderAftersaleLog::operAdd($order, $aftersale, $this->auth->getUserInfo(), 'admin', [ + 'reason' => '卖家同意退款(自动对齐)', + 'content' => '订单商品此前已完成退款,系统自动对齐售后单为已完成', + 'images' => [] + ]); + return; } // 实时剩余可退金额硬校验(防多退) diff --git a/tests/OrderRefundAutoAlignTest.php b/tests/OrderRefundAutoAlignTest.php new file mode 100644 index 0000000..43a473e --- /dev/null +++ b/tests/OrderRefundAutoAlignTest.php @@ -0,0 +1,93 @@ +testOrderOperRefundFinishAlignsAftersale(); + $this->testAftersaleRefundAutoAlignsWhenItemAlreadyRefunded(); + $this->testHuifuRefundServiceMultiDateAndSelfHealing(); + + echo "\n测试结果: {$this->passed} 通过, {$this->failed} 失败\n"; + if ($this->failed > 0) { + exit(1); + } + } + + private function assert($condition, $message) + { + if ($condition) { + echo " [PASS] {$message}\n"; + $this->passed++; + } else { + echo " [FAIL] {$message}\n"; + $this->failed++; + } + } + + /** + * 验证 OrderOper::refundFinish 会自动联动对齐该商品项下的待处理售后单 + */ + public function testOrderOperRefundFinishAlignsAftersale() + { + $filePath = __DIR__ . '/../addons/shopro/library/traits/model/order/OrderOper.php'; + $this->assert(is_file($filePath), "OrderOper.php 必须存在"); + + $content = file_get_contents($filePath); + $startPos = strpos($content, 'function refundFinish('); + $this->assert($startPos !== false, "OrderOper.php 必须包含 refundFinish 方法"); + + $nextFuncPos = strpos($content, 'public function ', $startPos + 20); + $methodBody = substr($content, $startPos, $nextFuncPos - $startPos); + + $hasAftersaleAlign = strpos($methodBody, 'OrderAftersale') !== false + && (strpos($methodBody, 'aftersale_status') !== false); + $this->assert($hasAftersaleAlign, "refundFinish 必须联动更新关联的 OrderAftersale 状态以实现自动对齐"); + } + + /** + * 验证 Aftersale 控制器的 refund 方法在商品已退款时自动对齐售后单状态,而不是直接抛异常卡死 + */ + public function testAftersaleRefundAutoAlignsWhenItemAlreadyRefunded() + { + $filePath = __DIR__ . '/../application/admin/controller/shopro/order/Aftersale.php'; + $content = file_get_contents($filePath); + + $startPos = strpos($content, 'function refund('); + $nextFuncPos = strpos($content, 'public function ', $startPos + 20); + $methodBody = substr($content, $startPos, $nextFuncPos - $startPos); + + // 检查当 $orderItem 已经退款时,是否进行售后单自动对齐保存 + $hasAutoAlign = strpos($methodBody, 'AFTERSALE_STATUS_OK') !== false + && strpos($methodBody, 'REFUND_STATUS_FINISH') !== false + && preg_match('/OrderAftersaleLog::operAdd.+?对齐/s', $methodBody); + + $this->assert((bool)$hasAutoAlign, "Aftersale.php 的 refund 方法在检测到商品已退款时,必须自动对齐售后单并记录对齐日志"); + } + + /** + * 验证 HuifuRefundService 支持跨日期核验(当天及原交易日)和全局异常自愈对齐 + */ + public function testHuifuRefundServiceMultiDateAndSelfHealing() + { + $filePath = __DIR__ . '/../addons/shopro_huifu/library/HuifuRefundService.php'; + $content = file_get_contents($filePath); + + $hasMultiDate = strpos($content, "date('Ymd')") !== false && strpos($content, "req_date") !== false; + $this->assert($hasMultiDate, "HuifuRefundService 幂等核验必须支持多日期(当天与原交易日)检查"); + + $hasSelfHealing = strpos($content, 'isAlreadyRefunded') !== false + && strpos($content, 'idempotent') !== false; + $this->assert($hasSelfHealing, "HuifuRefundService 必须包含汇付已退款场景的自动对齐自愈机制"); + } +} + +$test = new OrderRefundAutoAlignTest(); +$test->run();