fix: 完善退款完成售后单自动对齐机制与汇付多日期幂等自愈
这个提交包含在:
@@ -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();
|
||||
在新工单中引用
屏蔽一个用户