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)
+
+ | {{ 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') ?: '' }} |
+
+ @endforeach
+
+
+ @else
+
暂无退款记录。
+ @endif
+
+
+ 追加一条退款记录(会自动推进支付状态)
+
+
+
+
订阅同步记录
@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');
+ }
+}