fix: 优化退款防重与防多退机制,增加防重锁、事务排他行锁及汇付幂等对齐

这个提交包含在:
wikiboss
2026-09-12 17:18:43 +08:00
父节点 92622fc9b1
当前提交 2b996b8a2e
共有 4 个文件被更改,包括 190 次插入 和 63 次删除

查看文件

@@ -252,8 +252,8 @@ class Aftersale extends Backend
if ($this->request->isAjax()) {
$refund_money = round($this->request->post('refund_money', 0), 2);
if ($refund_money < 0) {
$this->error('退款金额不能小于 0');
if ($refund_money <= 0) {
$this->error('退款金额必须大于 0');
}
$aftersale = $this->model->withTrashed()->canOper()->where('id', $id)->find();
@@ -268,37 +268,34 @@ class Aftersale extends Backend
$this->error('订单不存在');
}
$items = $order->item;
$items = array_column($items, null, 'id');
// 旧的退款逻辑
// 当前订单已退款总金额
// $refunded_money = array_sum(array_column($items, 'refund_fee'));
// 剩余可退款金额
// $refund_surplus_money = $order->pay_fee - $refunded_money;
// 如果退款金额大于订单支付总金额
// if ($refund_money > $refund_surplus_money) {
// $this->error('退款总金额不能大于实际支付金额');
// }
// 新的退款逻辑
$orderItem = $items[$aftersale['order_item_id']];
// 剩余可退款金额
$refund_surplus_money = bcsub($orderItem->pay_price, $orderItem->refund_fee, 2);
if ($refund_money > $refund_surplus_money) {
$this->error('退款金额不能大于' . $refund_surplus_money . '元');
}
if (!$orderItem || in_array($orderItem['refund_status'], [
\app\admin\model\shopro\order\OrderItem::REFUND_STATUS_OK,
\app\admin\model\shopro\order\OrderItem::REFUND_STATUS_FINISH,
])) {
$this->error('订单商品已退款,不能重复退款');
$lockKey = 'lock:refund:aftersale:' . $id;
if (\think\Cache::get($lockKey)) {
$this->error('退款正在处理中,请勿重复操作');
}
\think\Cache::set($lockKey, 1, 10);
try {
Db::transaction(function () use ($aftersale, $order, $orderItem, $refund_money, $refund_surplus_money) {
Db::transaction(function () use ($id, $refund_money, $order) {
// 行级排他锁锁定售后单与订单商品,防止并发重复退款
$aftersale = $this->model->withTrashed()->where('id', $id)->lock(true)->find();
if (!$aftersale || $aftersale['refund_status'] == \app\admin\model\shopro\order\Aftersale::REFUND_STATUS_FINISH) {
throw new \Exception('售后单已退款,不能重复退款');
}
$orderItem = OrderItem::where('id', $aftersale['order_item_id'])->lock(true)->find();
if (!$orderItem || 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('订单商品已退款,不能重复退款');
}
// 实时剩余可退金额硬校验(防多退)
$refund_surplus_money = bcsub($orderItem->pay_price, $orderItem->refund_fee, 2);
if (bccomp($refund_money, $refund_surplus_money, 2) === 1) {
throw new \Exception('退款金额不能大于' . $refund_surplus_money . '元');
}
$data = ['aftersale' => $aftersale, 'order' => $order, 'item' => $orderItem];
\think\Hook::listen('aftersale_finish_before', $data);
@@ -325,9 +322,11 @@ class Aftersale extends Backend
\think\Hook::listen('aftersale_finish_after', $data);
});
} catch (\Throwable $e) {
\think\Cache::rm($lockKey);
$this->error($e->getMessage());
}
\think\Cache::rm($lockKey);
return $this->success('操作成功');
}
}

查看文件

@@ -1034,45 +1034,90 @@ class Order extends Base
}
}
$lockKey = 'lock:refund:order:' . $id;
if (\think\Cache::get($lockKey)) {
$this->error('退款正在处理中,请勿重复操作');
}
\think\Cache::set($lockKey, 1, 10);
try {
Db::transaction(function () use ($order, $items, $item_id, $refund_money, $refund_surplus_money) {
Db::transaction(function () use ($id, $item_id, $refund_money) {
// 行级排他锁锁定订单与商品项,防止并发重复退款与脏读
$order = $this->model->where('id', $id)->lock(true)->find();
if (!$order) {
throw new \Exception('订单不存在');
}
$lockedItems = \app\admin\model\shopro\order\OrderItem::where('order_id', $id)->lock(true)->select();
$items = array_column($lockedItems, null, 'id');
// 实时高精度计算已退金额与剩余可退金额
$refunded_money = array_sum(array_column($items, 'refund_fee'));
$refund_surplus_money = bcsub($order->pay_fee, $refunded_money, 2);
if (bccomp($refund_money, $refund_surplus_money, 2) === 1) {
throw new \Exception('退款金额不能大于剩余可退金额 ' . $refund_surplus_money . ' 元');
}
if ($item_id) {
// 单个商品退款
$item = $items[$item_id];
$item = $items[$item_id] ?? null;
if (!$item || in_array($item['refund_status'], [
\app\admin\model\shopro\order\OrderItem::REFUND_STATUS_OK,
\app\admin\model\shopro\order\OrderItem::REFUND_STATUS_FINISH,
])) {
throw new \Exception('订单商品已退款,不能重复退款');
}
$itemSurplus = bcsub($item->pay_price, $item->refund_fee, 2);
if (bccomp($refund_money, $itemSurplus, 2) === 1) {
throw new \Exception('退款金额不能大于商品剩余可退金额 ' . $itemSurplus . ' 元');
}
\app\admin\model\shopro\order\Order::startRefund($order, $item, $refund_money, $this->auth->getUserInfo(), '管理员操作退款');
} else {
// 全部退款
// 未退款 item 商品总金额
// 全部退款:过滤掉已退款的商品
$pendingItems = [];
$goods_total_amount = 0;
foreach ($items as $ke => $it) {
$goods_total_amount += ($it['goods_price'] * $it['goods_num']);
foreach ($items as $it) {
if (!in_array($it['refund_status'], [
\app\admin\model\shopro\order\OrderItem::REFUND_STATUS_OK,
\app\admin\model\shopro\order\OrderItem::REFUND_STATUS_FINISH,
])) {
$pendingItems[] = $it;
$goods_total_amount += ($it['goods_price'] * $it['goods_num']);
}
}
if (empty($pendingItems)) {
throw new \Exception('订单商品已全部退款,不能重复退款');
}
$current_refunded_money = 0;
for ($i = 0; $i < count($items); $i++) {
if ($i == (count($items) - 1)) {
$totalCount = count($pendingItems);
for ($i = 0; $i < $totalCount; $i++) {
if ($i == ($totalCount - 1)) {
// 最后一条,全部退完
$current_refund_money = $refund_money - $current_refunded_money;
$current_refund_money = bcsub($refund_money, $current_refunded_money, 2);
} else {
// 按比例计算当前 item 应退金额
$current_refund_money = round($refund_money * (($items[$i]['goods_price'] * $it['goods_num']) / $goods_total_amount), 2);
$current_refund_money = round($refund_money * (($pendingItems[$i]['goods_price'] * $pendingItems[$i]['goods_num']) / $goods_total_amount), 2);
}
if (($current_refunded_money + $current_refund_money) > $refund_money) {
$current_refund_money = $refund_money - $current_refunded_money;
if (bcadd($current_refunded_money, $current_refund_money, 2) > $refund_money) {
$current_refund_money = bcsub($refund_money, $current_refunded_money, 2);
}
if ($current_refund_money >= 0) { // 支付金额或者退款金额 为 0 也能退
$current_refunded_money += $current_refund_money;
\app\admin\model\shopro\order\Order::startRefund($order, $items[$i], $current_refund_money, $this->auth->getUserInfo(), '管理员操作退款');
if ($current_refund_money > 0) {
$current_refunded_money = bcadd($current_refunded_money, $current_refund_money, 2);
\app\admin\model\shopro\order\Order::startRefund($order, $pendingItems[$i], $current_refund_money, $this->auth->getUserInfo(), '管理员操作退款');
}
}
}
});
} catch (\Throwable $e) {
\think\Cache::rm($lockKey);
$this->error($e->getMessage());
}
\think\Cache::rm($lockKey);
$item_list = \app\admin\model\shopro\order\OrderItem::where(['order_id' => $id])->select();
return $this->success('操作成功', null, $item_list);
}