diff --git a/app/Http/Controllers/Admin/PlatformOrderController.php b/app/Http/Controllers/Admin/PlatformOrderController.php index 88826cf..1355f92 100644 --- a/app/Http/Controllers/Admin/PlatformOrderController.php +++ b/app/Http/Controllers/Admin/PlatformOrderController.php @@ -380,6 +380,60 @@ class PlatformOrderController extends Controller return redirect()->back()->with('success', '已追加支付回执记录(仅用于对账留痕,不自动改状态)。'); } + public function addRefundReceipt(Request $request, PlatformOrder $order): RedirectResponse + { + $admin = $this->ensurePlatformAdmin($request); + + $data = $request->validate([ + 'type' => ['required', 'string', 'max:30'], + 'channel' => ['nullable', 'string', 'max:30'], + 'amount' => ['required', 'numeric', 'min:0'], + 'refunded_at' => ['nullable', 'date'], + 'note' => ['nullable', 'string', 'max:2000'], + ]); + + $now = now(); + + $meta = (array) ($order->meta ?? []); + $refunds = (array) (data_get($meta, 'refund_receipts', []) ?? []); + + $refunds[] = [ + 'type' => (string) $data['type'], + 'channel' => (string) ($data['channel'] ?? ''), + 'amount' => (float) $data['amount'], + 'refunded_at' => $data['refunded_at'] ? (string) $data['refunded_at'] : null, + 'note' => (string) ($data['note'] ?? ''), + 'created_at' => $now->toDateTimeString(), + 'admin_id' => $admin->id, + ]; + + data_set($meta, 'refund_receipts', $refunds); + + // 可治理辅助:自动推进退款标记(仅当订单本身已支付,且退款金额>0 时) + if ($order->payment_status === 'paid' && (float) $data['amount'] > 0) { + $totalRefunded = 0.0; + foreach ($refunds as $r) { + $totalRefunded += (float) (data_get($r, 'amount') ?? 0); + } + + $paidAmount = (float) ($order->paid_amount ?? 0); + + // 退款总额 >= 已付金额 => 视为已退款;否则视为部分退款 + if ($paidAmount > 0 && $totalRefunded >= $paidAmount) { + $order->payment_status = 'refunded'; + $order->refunded_at = $order->refunded_at ?: now(); + } else { + $order->payment_status = 'partially_refunded'; + $order->refunded_at = $order->refunded_at ?: now(); + } + } + + $order->meta = $meta; + $order->save(); + + return redirect()->back()->with('success', '已追加退款记录(用于退款轨迹留痕)。'); + } + public function markActivated(Request $request, PlatformOrder $order): RedirectResponse { $admin = $this->ensurePlatformAdmin($request); @@ -479,6 +533,10 @@ class PlatformOrderController extends Controller '最近回执时间', '最近回执金额', '最近回执渠道', + '退款记录数', + '最近退款时间', + '最近退款金额', + '最近退款渠道', ]; if ($includeMeta) { @@ -504,6 +562,10 @@ class PlatformOrderController extends Controller $receiptCount = count($receipts); $latestReceipt = $receiptCount > 0 ? end($receipts) : null; + $refunds = (array) (data_get($order->meta, 'refund_receipts', []) ?? []); + $refundCount = count($refunds); + $latestRefund = $refundCount > 0 ? end($refunds) : null; + $row = [ $order->id, $order->order_no, @@ -530,6 +592,10 @@ class PlatformOrderController extends Controller (string) (data_get($latestReceipt, 'paid_at') ?? ''), (float) (data_get($latestReceipt, 'amount') ?? 0), (string) (data_get($latestReceipt, 'channel') ?? ''), + $refundCount, + (string) (data_get($latestRefund, 'refunded_at') ?? ''), + (float) (data_get($latestRefund, 'amount') ?? 0), + (string) (data_get($latestRefund, 'channel') ?? ''), ]; if ($includeMeta) { diff --git a/resources/views/admin/platform_orders/show.blade.php b/resources/views/admin/platform_orders/show.blade.php index 4633055..ba3bda1 100644 --- a/resources/views/admin/platform_orders/show.blade.php +++ b/resources/views/admin/platform_orders/show.blade.php @@ -87,6 +87,7 @@ $activationError = data_get($order->meta, 'subscription_activation_error'); $audit = (array) (data_get($order->meta, 'audit', []) ?? []); $paymentReceipts = (array) (data_get($order->meta, 'payment_receipts', []) ?? []); + $refundReceipts = (array) (data_get($order->meta, 'refund_receipts', []) ?? []); @endphp @php @@ -166,6 +167,64 @@ +
+

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

+

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

+ + @if(count($refundReceipts) > 0) + @php $items = array_slice(array_reverse($refundReceipts), 0, 20); @endphp + + + + + + + + + + + + + + @foreach($items as $r) + + + + + + + + + + @endforeach + +
类型渠道金额退款时间记录时间管理员备注
{{ data_get($r, 'type') ?: '-' }}{{ data_get($r, 'channel') ?: '-' }}¥{{ number_format((float) (data_get($r, 'amount') ?? 0), 2) }}{{ data_get($r, 'refunded_at') ?: '-' }}{{ data_get($r, 'created_at') ?: '-' }}{{ data_get($r, 'admin_id') ?: '-' }}{{ data_get($r, 'note') ?: '' }}
+ @else +

暂无退款记录。

+ @endif + +
+ 追加一条退款记录(会自动推进支付状态) +
+ @csrf +
+ + + +
+
+ +
+
+ +
+
+ +
+
+
+
+

订阅同步记录

@if($activation) diff --git a/routes/web.php b/routes/web.php index 025d81b..a963b83 100644 --- a/routes/web.php +++ b/routes/web.php @@ -110,6 +110,7 @@ Route::prefix('admin')->group(function () { Route::post('/platform-orders/{order}/activate-subscription', [PlatformOrderController::class, 'activateSubscription']); 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-activated', [PlatformOrderController::class, 'markActivated']); Route::get('/site-subscriptions', [SiteSubscriptionController::class, 'index']); diff --git a/tests/Feature/AdminPlatformOrderRefundReceiptTest.php b/tests/Feature/AdminPlatformOrderRefundReceiptTest.php new file mode 100644 index 0000000..9a70a35 --- /dev/null +++ b/tests/Feature/AdminPlatformOrderRefundReceiptTest.php @@ -0,0 +1,168 @@ +seed(); + + $this->post('/admin/login', [ + 'email' => 'platform.admin@demo.local', + 'password' => 'Platform@123456', + ])->assertRedirect('/admin'); + } + + public function test_platform_admin_can_add_refund_receipt_and_mark_partially_refunded(): void + { + $this->loginAsPlatformAdmin(); + + $merchant = Merchant::query()->firstOrFail(); + $plan = Plan::query()->create([ + 'code' => 'refund_test', + 'name' => '退款测试(月付)', + 'billing_cycle' => 'monthly', + 'price' => 30, + 'list_price' => 30, + 'status' => 'active', + 'sort' => 10, + 'published_at' => now(), + ]); + + $order = PlatformOrder::query()->create([ + 'merchant_id' => $merchant->id, + 'plan_id' => $plan->id, + 'order_no' => 'PO_REFUND_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' => 30, + 'paid_amount' => 30, + 'placed_at' => now(), + 'paid_at' => now(), + 'activated_at' => now(), + ]); + + $this->post('/admin/platform-orders/' . $order->id . '/add-refund-receipt', [ + 'type' => 'refund', + 'channel' => 'alipay', + 'amount' => 10, + 'refunded_at' => now()->format('Y-m-d H:i:s'), + 'note' => '部分退款', + ])->assertRedirect(); + + $order->refresh(); + + $refunds = (array) data_get($order->meta, 'refund_receipts', []); + $this->assertCount(1, $refunds); + $this->assertSame('refund', data_get($refunds[0], 'type')); + $this->assertSame('alipay', data_get($refunds[0], 'channel')); + $this->assertSame(10.0, (float) data_get($refunds[0], 'amount')); + + $this->assertSame('partially_refunded', $order->payment_status); + $this->assertNotNull($order->refunded_at); + } + + public function test_platform_admin_can_add_refund_receipt_and_mark_fully_refunded_when_total_refund_reaches_paid_amount(): void + { + $this->loginAsPlatformAdmin(); + + $merchant = Merchant::query()->firstOrFail(); + $plan = Plan::query()->create([ + 'code' => 'refund_test_full', + 'name' => '退款测试(全额)', + 'billing_cycle' => 'monthly', + 'price' => 30, + 'list_price' => 30, + 'status' => 'active', + 'sort' => 10, + 'published_at' => now(), + ]); + + $order = PlatformOrder::query()->create([ + 'merchant_id' => $merchant->id, + 'plan_id' => $plan->id, + 'order_no' => 'PO_REFUND_0002', + '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' => 30, + 'paid_amount' => 30, + 'placed_at' => now(), + 'paid_at' => now(), + 'activated_at' => now(), + ]); + + $this->post('/admin/platform-orders/' . $order->id . '/add-refund-receipt', [ + 'type' => 'refund', + 'channel' => 'wechat', + 'amount' => 30, + 'refunded_at' => now()->format('Y-m-d H:i:s'), + 'note' => '全额退款', + ])->assertRedirect(); + + $order->refresh(); + $this->assertSame('refunded', $order->payment_status); + $this->assertNotNull($order->refunded_at); + + $refunds = (array) data_get($order->meta, 'refund_receipts', []); + $this->assertCount(1, $refunds); + } + + public function test_guest_cannot_add_refund_receipt(): void + { + $this->seed(); + + $merchant = Merchant::query()->firstOrFail(); + $plan = Plan::query()->create([ + 'code' => 'refund_test_guest', + 'name' => '退款测试(游客)', + 'billing_cycle' => 'monthly', + 'price' => 30, + 'list_price' => 30, + 'status' => 'active', + 'sort' => 10, + 'published_at' => now(), + ]); + + $order = PlatformOrder::query()->create([ + 'merchant_id' => $merchant->id, + 'plan_id' => $plan->id, + 'order_no' => 'PO_REFUND_0003', + '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' => 30, + 'paid_amount' => 30, + 'placed_at' => now(), + 'paid_at' => now(), + 'activated_at' => now(), + ]); + + $this->post('/admin/platform-orders/' . $order->id . '/add-refund-receipt', [ + 'type' => 'refund', + 'amount' => 10, + ])->assertRedirect('/admin/login'); + } +}