fix: 完善退款完成售后单自动对齐机制与汇付多日期幂等自愈

这个提交包含在:
wikiboss
2026-09-12 17:48:22 +08:00
父节点 2b996b8a2e
当前提交 65290f5742
共有 4 个文件被更改,包括 164 次插入 和 15 次删除

查看文件

@@ -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', '退款成功');
}

查看文件

@@ -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;
}

查看文件

@@ -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;
}
// 实时剩余可退金额硬校验(防多退)

查看文件

@@ -0,0 +1,93 @@
<?php
/**
* 订单与售后退款自动对齐单元测试 (TDD)
*/
class OrderRefundAutoAlignTest
{
private $passed = 0;
private $failed = 0;
public function run()
{
echo "=== 开始执行 订单与售后退款自动对齐单元测试 (TDD) ===\n";
$this->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();