diff --git a/app/Http/Controllers/Admin/PlatformOrderController.php b/app/Http/Controllers/Admin/PlatformOrderController.php index 0563ff6..ff34284 100644 --- a/app/Http/Controllers/Admin/PlatformOrderController.php +++ b/app/Http/Controllers/Admin/PlatformOrderController.php @@ -580,6 +580,39 @@ class PlatformOrderController extends Controller return redirect()->back()->with('success', '已追加退款记录(用于退款轨迹留痕)。'); } + public function markRefunded(Request $request, PlatformOrder $order): RedirectResponse + { + $admin = $this->ensurePlatformAdmin($request); + + if ((float) ($order->paid_amount ?? 0) <= 0) { + return redirect()->back()->with('warning', '当前订单已付金额为 0,无法标记为已退款。'); + } + + if ((string) $order->payment_status === 'refunded') { + return redirect()->back()->with('warning', '当前订单已是已退款状态,无需重复操作。'); + } + + $now = now(); + $order->payment_status = 'refunded'; + $order->refunded_at = $order->refunded_at ?: $now; + + $meta = (array) ($order->meta ?? []); + $audit = (array) (data_get($meta, 'audit', []) ?? []); + $audit[] = [ + 'action' => 'mark_refunded', + 'scope' => 'single', + 'at' => $now->toDateTimeString(), + 'admin_id' => $admin->id, + 'note' => '手动标记为已退款(仅修正支付状态,不自动写退款回执)', + ]; + data_set($meta, 'audit', $audit); + + $order->meta = $meta; + $order->save(); + + return redirect()->back()->with('success', '已将订单支付状态标记为已退款(未自动写入退款回执)。'); + } + public function markActivated(Request $request, PlatformOrder $order): RedirectResponse { $admin = $this->ensurePlatformAdmin($request); diff --git a/resources/views/admin/platform_orders/show.blade.php b/resources/views/admin/platform_orders/show.blade.php index 3cbe806..78e048e 100644 --- a/resources/views/admin/platform_orders/show.blade.php +++ b/resources/views/admin/platform_orders/show.blade.php @@ -79,7 +79,23 @@ @php $paidAmountFloat = (float) ($order->paid_amount ?? 0); + + // 若订单疑似退款不一致: + // - 非 refunded 但退款总额已达/超已付 => 给出“可一键标记为已退款”的治理动作(不自动写回执) + $canMarkRefunded = $paidAmountFloat > 0 + && $order->payment_status !== 'refunded' + && round($refundTotal * 100) >= round($paidAmountFloat * 100); @endphp + @if($canMarkRefunded) +
+ 提示:退款总额已达到/超过已付金额,但支付状态尚未是「已退款」,如确认无误,可直接修正。 +
+ @csrf + +
+
+ @endif + @if($order->payment_status === 'refunded' && ($refundTotal + 0.01) < $paidAmountFloat)
提示:当前订单状态为「已退款」,但退款总额小于已付金额,可能存在数据不一致,请核对退款轨迹与订单金额。
@elseif($order->payment_status !== 'refunded' && $paidAmountFloat > 0 && $refundTotal >= $paidAmountFloat) @@ -260,6 +276,27 @@

退款记录(退款轨迹留痕)

用于记录退款动作与对账轨迹(先落 meta,不引入独立表)。追加退款后,系统会自动把支付状态推进为“部分退款/已退款”(仅在订单当前为已支付时)。

+ @php + // 退款不一致(与列表/筛选口径保持一致,按分取整 + 0.01 容差) + $paidAmountFloat3 = (float) ($order->paid_amount ?? 0); + $isRefundInconsistent3 = false; + if ($paidAmountFloat3 > 0) { + if ((string) $order->payment_status === 'refunded') { + $isRefundInconsistent3 = (round($refundTotal * 100) + 1) < round($paidAmountFloat3 * 100); + } else { + $isRefundInconsistent3 = round($refundTotal * 100) >= round($paidAmountFloat3 * 100); + } + } + @endphp + + @if($isRefundInconsistent3) +
+ 提示:该订单疑似存在「退款状态 vs 退款总额」不一致,建议核对退款轨迹、已付金额与支付状态。 + + 查看全部退款不一致订单 +
+ @endif + @if(count($refundReceipts) > 0) @php $items = array_slice(array_reverse($refundReceipts), 0, 20); @endphp @@ -355,6 +392,7 @@ 'mark_activated' => '仅标记为已生效', 'batch_mark_activated' => '批量仅标记为已生效', 'activate_subscription' => '同步订阅', + 'mark_refunded' => '手动标记为已退款', ]; @endphp
diff --git a/routes/web.php b/routes/web.php index 3c9e725..eed155a 100644 --- a/routes/web.php +++ b/routes/web.php @@ -111,6 +111,7 @@ Route::prefix('admin')->group(function () { Route::post('/platform-orders/{order}/mark-paid-and-activate', [PlatformOrderController::class, 'markPaidAndActivate']); Route::post('/platform-orders/{order}/add-payment-receipt', [PlatformOrderController::class, 'addPaymentReceipt']); Route::post('/platform-orders/{order}/add-refund-receipt', [PlatformOrderController::class, 'addRefundReceipt']); + Route::post('/platform-orders/{order}/mark-refunded', [PlatformOrderController::class, 'markRefunded']); Route::post('/platform-orders/{order}/mark-activated', [PlatformOrderController::class, 'markActivated']); Route::get('/site-subscriptions', [SiteSubscriptionController::class, 'index']); diff --git a/tests/Feature/AdminPlatformOrderMarkRefundedTest.php b/tests/Feature/AdminPlatformOrderMarkRefundedTest.php new file mode 100644 index 0000000..c0dbbd7 --- /dev/null +++ b/tests/Feature/AdminPlatformOrderMarkRefundedTest.php @@ -0,0 +1,74 @@ +seed(); + + $this->post('/admin/login', [ + 'email' => 'platform.admin@demo.local', + 'password' => 'Platform@123456', + ])->assertRedirect('/admin'); + } + + public function test_platform_admin_can_mark_order_as_refunded(): void + { + $this->loginAsPlatformAdmin(); + + $merchant = Merchant::query()->firstOrFail(); + $plan = Plan::query()->create([ + 'code' => 'mark_refunded_test_plan', + 'name' => '标记已退款测试套餐', + 'billing_cycle' => 'monthly', + 'price' => 10, + 'list_price' => 10, + 'status' => 'active', + 'sort' => 10, + 'published_at' => now(), + ]); + + $order = PlatformOrder::query()->create([ + 'merchant_id' => $merchant->id, + 'plan_id' => $plan->id, + 'order_no' => 'PO_MARK_REFUNDED_0001', + 'order_type' => 'new_purchase', + 'status' => 'activated', + 'payment_status' => 'paid', + 'plan_name' => $plan->name, + 'billing_cycle' => $plan->billing_cycle, + 'period_months' => 1, + 'quantity' => 1, + 'payable_amount' => 10, + 'paid_amount' => 10, + 'placed_at' => now(), + 'paid_at' => now(), + 'activated_at' => now(), + 'meta' => [ + 'refund_summary' => [ + 'count' => 1, + 'total_amount' => 10.00, + ], + ], + ]); + + $this->post('/admin/platform-orders/' . $order->id . '/mark-refunded') + ->assertRedirect(); + + $order->refresh(); + + $this->assertSame('refunded', $order->payment_status); + $this->assertNotNull($order->refunded_at); + $this->assertSame('mark_refunded', (string) data_get($order->meta, 'audit.0.action')); + } +}