From 3f809c8150e73da6c38427bc1800ad042b2ab81f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=90=9D=E5=8D=9C?= Date: Tue, 10 Mar 2026 14:35:31 +0000 Subject: [PATCH] =?UTF-8?q?feat(platform-orders):=20=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E6=80=BB=E5=8F=B0=E6=89=8B=E5=B7=A5=E5=88=9B=E5=BB=BA=E5=B9=B3?= =?UTF-8?q?=E5=8F=B0=E8=AE=A2=E5=8D=95=E5=B9=B6=E8=BF=9B=E5=85=A5=E9=97=AD?= =?UTF-8?q?=E7=8E=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Admin/PlatformOrderController.php | 117 ++++++++++++++++++ .../admin/platform_orders/form.blade.php | 74 +++++++++++ .../admin/platform_orders/index.blade.php | 5 +- routes/web.php | 2 + .../Feature/AdminPlatformOrderCreateTest.php | 107 ++++++++++++++++ 5 files changed, 304 insertions(+), 1 deletion(-) create mode 100644 resources/views/admin/platform_orders/form.blade.php create mode 100644 tests/Feature/AdminPlatformOrderCreateTest.php diff --git a/app/Http/Controllers/Admin/PlatformOrderController.php b/app/Http/Controllers/Admin/PlatformOrderController.php index 22f3a73..ad513f0 100644 --- a/app/Http/Controllers/Admin/PlatformOrderController.php +++ b/app/Http/Controllers/Admin/PlatformOrderController.php @@ -4,12 +4,15 @@ namespace App\Http\Controllers\Admin; use App\Http\Controllers\Concerns\ResolvesPlatformAdminContext; use App\Http\Controllers\Controller; +use App\Models\Merchant; +use App\Models\Plan; use App\Models\PlatformOrder; use App\Support\SubscriptionActivationService; use Illuminate\Database\Eloquent\Builder; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; +use Illuminate\Validation\Rule; use Illuminate\View\View; use Symfony\Component\HttpFoundation\StreamedResponse; @@ -17,6 +20,89 @@ class PlatformOrderController extends Controller { use ResolvesPlatformAdminContext; + public function create(Request $request): View + { + $this->ensurePlatformAdmin($request); + + $merchants = Merchant::query()->orderBy('id')->get(['id', 'name']); + $plans = Plan::query()->orderBy('sort')->orderByDesc('id')->get(); + + return view('admin.platform_orders.form', [ + 'merchants' => $merchants, + 'plans' => $plans, + 'billingCycleLabels' => $this->billingCycleLabels(), + 'orderTypeLabels' => $this->orderTypeLabels(), + ]); + } + + public function store(Request $request): RedirectResponse + { + $admin = $this->ensurePlatformAdmin($request); + + $data = $request->validate([ + 'merchant_id' => ['required', 'integer', 'exists:merchants,id'], + 'plan_id' => ['required', 'integer', 'exists:plans,id'], + 'order_type' => ['required', Rule::in(array_keys($this->orderTypeLabels()))], + 'quantity' => ['required', 'integer', 'min:1', 'max:120'], + 'discount_amount' => ['nullable', 'numeric', 'min:0'], + 'payment_channel' => ['nullable', 'string', 'max:30'], + 'remark' => ['nullable', 'string', 'max:2000'], + ]); + + $plan = Plan::query()->findOrFail((int) $data['plan_id']); + + $periodMonths = $this->periodMonthsFromBillingCycle((string) $plan->billing_cycle); + $quantity = (int) $data['quantity']; + + $listAmount = (float) $plan->price * $quantity; + $discount = (float) ($data['discount_amount'] ?? 0); + $discount = max(0, min($listAmount, $discount)); + + $payable = max(0, $listAmount - $discount); + + $now = now(); + + // 订单号:PO + 时间 + 4位随机数(足够用于当前阶段演示与手工补单) + $orderNo = 'PO' . $now->format('YmdHis') . str_pad((string) random_int(1, 9999), 4, '0', STR_PAD_LEFT); + + $order = PlatformOrder::query()->create([ + 'merchant_id' => (int) $data['merchant_id'], + 'plan_id' => $plan->id, + 'created_by_admin_id' => $admin->id, + 'order_no' => $orderNo, + 'order_type' => (string) $data['order_type'], + 'status' => 'pending', + 'payment_status' => 'unpaid', + 'payment_channel' => $data['payment_channel'] ?? null, + 'plan_name' => $plan->name, + 'billing_cycle' => $plan->billing_cycle, + 'period_months' => $periodMonths, + 'quantity' => $quantity, + 'list_amount' => $listAmount, + 'discount_amount' => $discount, + 'payable_amount' => $payable, + 'paid_amount' => 0, + 'placed_at' => $now, + 'plan_snapshot' => [ + 'plan_id' => $plan->id, + 'code' => $plan->code, + 'name' => $plan->name, + 'billing_cycle' => $plan->billing_cycle, + 'price' => (float) $plan->price, + 'list_price' => (float) $plan->list_price, + 'status' => $plan->status, + 'published_at' => optional($plan->published_at)->toDateTimeString(), + ], + 'meta' => [ + 'created_from' => 'manual_form', + ], + 'remark' => $data['remark'] ?? null, + ]); + + return redirect('/admin/platform-orders/' . $order->id) + ->with('success', '平台订单已创建:' . $order->order_no . '(待支付/待生效)'); + } + public function index(Request $request): View { $this->ensurePlatformAdmin($request); @@ -610,4 +696,35 @@ class PlatformOrderController extends Controller 'failed' => '支付失败', ]; } + + protected function orderTypeLabels(): array + { + return [ + 'new_purchase' => '新购', + 'renewal' => '续费', + 'upgrade' => '升级', + 'downgrade' => '降级', + ]; + } + + protected function billingCycleLabels(): array + { + return [ + 'monthly' => '月付', + 'quarterly' => '季付', + 'yearly' => '年付', + 'one_time' => '一次性', + ]; + } + + protected function periodMonthsFromBillingCycle(string $billingCycle): int + { + return match ($billingCycle) { + 'monthly' => 1, + 'quarterly' => 3, + 'yearly' => 12, + 'one_time' => 1, + default => 1, + }; + } } diff --git a/resources/views/admin/platform_orders/form.blade.php b/resources/views/admin/platform_orders/form.blade.php new file mode 100644 index 0000000..1350833 --- /dev/null +++ b/resources/views/admin/platform_orders/form.blade.php @@ -0,0 +1,74 @@ +@extends('admin.layouts.app') + +@section('title', '新建平台订单') +@section('page_title', '新建平台订单') + +@section('content') +
+

用于总台运营手工创建一笔平台订单(演示/补单/线下收款录入)。

+

创建后可在「平台订单」列表中继续推进:标记支付并生效 → 同步订阅(形成最小收费闭环)。

+
+ +
+ @csrf + + + + + + + + + + + + + + + +
+ 返回 + +
+
+@endsection diff --git a/resources/views/admin/platform_orders/index.blade.php b/resources/views/admin/platform_orders/index.blade.php index 96410d1..888a26a 100644 --- a/resources/views/admin/platform_orders/index.blade.php +++ b/resources/views/admin/platform_orders/index.blade.php @@ -211,7 +211,10 @@
-

平台订单列表

+
+

平台订单列表

+ 新建平台订单 +
diff --git a/routes/web.php b/routes/web.php index a5379b1..83439bd 100644 --- a/routes/web.php +++ b/routes/web.php @@ -101,6 +101,8 @@ Route::prefix('admin')->group(function () { Route::get('/platform-orders', [PlatformOrderController::class, 'index']); Route::get('/platform-orders/export', [PlatformOrderController::class, 'export']); + Route::get('/platform-orders/create', [PlatformOrderController::class, 'create']); + Route::post('/platform-orders', [PlatformOrderController::class, 'store']); Route::post('/platform-orders/batch-activate-subscriptions', [PlatformOrderController::class, 'batchActivateSubscriptions']); Route::post('/platform-orders/clear-sync-errors', [PlatformOrderController::class, 'clearSyncErrors']); Route::get('/platform-orders/{order}', [PlatformOrderController::class, 'show']); diff --git a/tests/Feature/AdminPlatformOrderCreateTest.php b/tests/Feature/AdminPlatformOrderCreateTest.php new file mode 100644 index 0000000..d576b8b --- /dev/null +++ b/tests/Feature/AdminPlatformOrderCreateTest.php @@ -0,0 +1,107 @@ +seed(); + + $this->post('/admin/login', [ + 'email' => 'platform.admin@demo.local', + 'password' => 'Platform@123456', + ])->assertRedirect('/admin'); + } + + public function test_platform_admin_can_open_create_platform_order_form(): void + { + $this->loginAsPlatformAdmin(); + + // 需要至少一个套餐供选择 + Plan::query()->create([ + 'code' => 'create_order_plan_01', + 'name' => '创建订单测试套餐', + 'billing_cycle' => 'monthly', + 'price' => 100, + 'list_price' => 100, + 'status' => 'active', + 'sort' => 10, + 'published_at' => now(), + ]); + + $res = $this->get('/admin/platform-orders/create'); + + $res->assertOk(); + $res->assertSee('新建平台订单'); + $res->assertSee('站点'); + $res->assertSee('套餐'); + $res->assertSee('创建订单'); + } + + public function test_platform_admin_can_create_platform_order_and_see_it_in_show_page(): void + { + $this->loginAsPlatformAdmin(); + + $merchant = Merchant::query()->firstOrFail(); + + $plan = Plan::query()->create([ + 'code' => 'create_order_plan_02', + 'name' => '创建订单测试套餐2', + 'billing_cycle' => 'monthly', + 'price' => 199, + 'list_price' => 199, + 'status' => 'active', + 'sort' => 10, + 'published_at' => now(), + ]); + + $res = $this->post('/admin/platform-orders', [ + 'merchant_id' => $merchant->id, + 'plan_id' => $plan->id, + 'order_type' => 'new_purchase', + 'quantity' => 2, + 'discount_amount' => 10, + 'payment_channel' => 'offline', + 'remark' => '线下补单', + ]); + + $res->assertRedirect(); + + $order = PlatformOrder::query()->latest('id')->first(); + $this->assertNotNull($order); + $this->assertSame($merchant->id, $order->merchant_id); + $this->assertSame($plan->id, $order->plan_id); + $this->assertSame('pending', $order->status); + $this->assertSame('unpaid', $order->payment_status); + $this->assertSame('offline', $order->payment_channel); + + // 金额口径:list_amount=price*quantity,payable=list-discount + $this->assertSame(398.0, (float) $order->list_amount); + $this->assertSame(10.0, (float) $order->discount_amount); + $this->assertSame(388.0, (float) $order->payable_amount); + + $show = $this->get('/admin/platform-orders/' . $order->id); + $show->assertOk(); + $show->assertSee($order->order_no); + $show->assertSee('平台订单详情'); + $show->assertSee('待处理'); + $show->assertSee('未支付'); + } + + public function test_guest_cannot_open_create_platform_order_form(): void + { + $this->seed(); + + $this->get('/admin/platform-orders/create') + ->assertRedirect('/admin/login'); + } +}