From 4d41313f8f733cc1c12ea523ba4605294b873323 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=90=9D=E5=8D=9C?= Date: Tue, 10 Mar 2026 22:38:26 +0000 Subject: [PATCH] =?UTF-8?q?feat(platform-orders):=20=E5=AF=B9=E8=B4=A6?= =?UTF-8?q?=E4=B8=8D=E4=B8=80=E8=87=B4=E7=AD=9B=E9=80=89=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E5=9B=9E=E9=80=80=E6=B1=87=E6=80=BB=20payment=5Freceipts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Admin/PlatformOrderController.php | 11 +- ...econcileMismatchFallbackToReceiptsTest.php | 108 ++++++++++++++++++ 2 files changed, 114 insertions(+), 5 deletions(-) create mode 100644 tests/Feature/AdminPlatformOrderReconcileMismatchFallbackToReceiptsTest.php diff --git a/app/Http/Controllers/Admin/PlatformOrderController.php b/app/Http/Controllers/Admin/PlatformOrderController.php index 54e4c35..b37498a 100644 --- a/app/Http/Controllers/Admin/PlatformOrderController.php +++ b/app/Http/Controllers/Admin/PlatformOrderController.php @@ -1101,18 +1101,19 @@ class PlatformOrderController extends Controller } }) ->when(($filters['reconcile_mismatch'] ?? '') !== '', function (Builder $builder) { - // 只看“对账不一致”的订单(粗版):支付回执总额(优先 payment_summary.total_amount)与订单 paid_amount 不一致 + // 只看“对账不一致”的订单:支付回执总额 与订单 paid_amount 不一致 + // 口径:优先使用 meta.payment_summary.total_amount;若为空,则回退汇总 meta.payment_receipts[].amount // 注意:该筛选需要读取 JSON 字段,MySQL/SQLite 写法略有差异。 $driver = $builder->getQuery()->getConnection()->getDriverName(); if ($driver === 'sqlite') { // sqlite 下 JSON_EXTRACT 直接返回标量(数值或字符串),这里用“按分”取整避免浮点误差导致 0.01 边界不稳定 - $builder->whereRaw("JSON_EXTRACT(meta, '$.payment_summary.total_amount') IS NOT NULL") - ->whereRaw("ABS(ROUND(CAST(JSON_EXTRACT(meta, '$.payment_summary.total_amount') AS REAL) * 100) - ROUND(paid_amount * 100)) >= 1"); + // total_cents = (payment_summary.total_amount 存在 ? summary*100 : sum(payment_receipts[].amount)*100) + $builder->whereRaw("ABS(ROUND((CASE WHEN JSON_EXTRACT(meta, '$.payment_summary.total_amount') IS NOT NULL THEN CAST(JSON_EXTRACT(meta, '$.payment_summary.total_amount') AS REAL) ELSE (SELECT IFNULL(SUM(CAST(JSON_EXTRACT(value, '$.amount') AS REAL)), 0) FROM json_each(COALESCE(JSON_EXTRACT(meta, '$.payment_receipts'), '[]'))) END) * 100) - ROUND(paid_amount * 100)) >= 1"); } else { // MySQL 下 JSON_EXTRACT 返回 JSON,需要 JSON_UNQUOTE 再 cast;同样按分取整避免浮点误差 - $builder->whereRaw("JSON_EXTRACT(meta, '$.payment_summary.total_amount') IS NOT NULL") - ->whereRaw("ABS(ROUND(CAST(JSON_UNQUOTE(JSON_EXTRACT(meta, '$.payment_summary.total_amount')) AS DECIMAL(12,2)) * 100) - ROUND(paid_amount * 100)) >= 1"); + // total_cents = (payment_summary.total_amount 存在 ? summary*100 : SUM(payment_receipts[].amount)*100) + $builder->whereRaw("ABS(ROUND((CASE WHEN JSON_EXTRACT(meta, '$.payment_summary.total_amount') IS NOT NULL THEN CAST(JSON_UNQUOTE(JSON_EXTRACT(meta, '$.payment_summary.total_amount')) AS DECIMAL(12,2)) ELSE (SELECT IFNULL(SUM(j.amount), 0) FROM JSON_TABLE(meta, '$.payment_receipts[*]' COLUMNS(amount DECIMAL(12,2) PATH '$.amount')) j) END) * 100) - ROUND(paid_amount * 100)) >= 1"); } }); } diff --git a/tests/Feature/AdminPlatformOrderReconcileMismatchFallbackToReceiptsTest.php b/tests/Feature/AdminPlatformOrderReconcileMismatchFallbackToReceiptsTest.php new file mode 100644 index 0000000..81ba755 --- /dev/null +++ b/tests/Feature/AdminPlatformOrderReconcileMismatchFallbackToReceiptsTest.php @@ -0,0 +1,108 @@ +seed(); + + $this->post('/admin/login', [ + 'email' => 'platform.admin@demo.local', + 'password' => 'Platform@123456', + ])->assertRedirect('/admin'); + } + + public function test_reconcile_mismatch_filter_falls_back_to_payment_receipts_when_no_payment_summary(): void + { + $this->loginAsPlatformAdmin(); + + $merchant = Merchant::query()->firstOrFail(); + $plan = Plan::query()->create([ + 'code' => 'reconcile_mismatch_receipts_fallback', + 'name' => '对账不一致回退回执汇总测试', + 'billing_cycle' => 'monthly', + 'price' => 10, + 'list_price' => 10, + 'status' => 'active', + 'sort' => 10, + 'published_at' => now(), + ]); + + // 不一致:paid_amount=10,但 payment_receipts 合计=9.99(且没有 payment_summary) + PlatformOrder::query()->create([ + 'merchant_id' => $merchant->id, + 'plan_id' => $plan->id, + 'order_no' => 'PO_RECON_FALLBACK_MISMATCH_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' => [ + 'payment_receipts' => [ + [ + 'type' => 'bank_transfer', + 'channel' => 'bank', + 'amount' => 9.99, + 'paid_at' => now()->toDateTimeString(), + 'created_at' => now()->toDateTimeString(), + 'admin_id' => 1, + ], + ], + ], + ]); + + // 一致:paid_amount=10,payment_receipts 合计=10(且没有 payment_summary) + PlatformOrder::query()->create([ + 'merchant_id' => $merchant->id, + 'plan_id' => $plan->id, + 'order_no' => 'PO_RECON_FALLBACK_MATCH_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' => 10, + 'paid_amount' => 10, + 'placed_at' => now(), + 'paid_at' => now(), + 'activated_at' => now(), + 'meta' => [ + 'payment_receipts' => [ + [ + 'type' => 'bank_transfer', + 'channel' => 'bank', + 'amount' => 10.00, + 'paid_at' => now()->toDateTimeString(), + 'created_at' => now()->toDateTimeString(), + 'admin_id' => 1, + ], + ], + ], + ]); + + $this->get('/admin/platform-orders?reconcile_mismatch=1') + ->assertOk() + ->assertSee('PO_RECON_FALLBACK_MISMATCH_0001') + ->assertDontSee('PO_RECON_FALLBACK_MATCH_0002'); + } +}