diff --git a/app/Http/Controllers/Admin/PlatformOrderController.php b/app/Http/Controllers/Admin/PlatformOrderController.php index 0da9282..f4a7a5d 100644 --- a/app/Http/Controllers/Admin/PlatformOrderController.php +++ b/app/Http/Controllers/Admin/PlatformOrderController.php @@ -1385,7 +1385,11 @@ class PlatformOrderController extends Controller if ($driver === 'sqlite') { // sqlite 下 JSON_EXTRACT 直接返回标量(数值或字符串),这里用“按分”取整避免浮点误差导致 0.01 边界不稳定 // 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"); + $tol = (float) config('saasshop.amounts.tolerance', 0.01); + $tolCents = (int) round($tol * 100); + $tolCents = max(1, $tolCents); + + $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)) >= {$tolCents}"); } else { // MySQL 下 JSON_EXTRACT 返回 JSON,需要 JSON_UNQUOTE 再 cast;同样按分取整避免浮点误差 // total_cents = (payment_summary.total_amount 存在 ? summary*100 : SUM(payment_receipts[].amount)*100) diff --git a/app/Models/PlatformOrder.php b/app/Models/PlatformOrder.php index d4853d3..35316c9 100644 --- a/app/Models/PlatformOrder.php +++ b/app/Models/PlatformOrder.php @@ -65,7 +65,17 @@ class PlatformOrder extends Model public function isReconcileMismatch(): bool { - // 口径与平台订单列表 reconcile_mismatch 保持一致:支付回执总额 与订单 paid_amount 不一致(按分取整,差额>=0.01) + // 口径与平台订单列表 reconcile_mismatch 保持一致:支付回执总额 与订单 paid_amount 不一致(按分取整,差额>=容差) + // + // 重要:若该订单尚无任何“回执证据”(payment_summary/payment_receipts 都为空),则不判定为对账不一致。 + // 原因:测试/人工补数据场景下,订单可能已标记 paid,但尚未沉淀回执;此时应通过 receipt_status=none 暴露问题, + // 而不是把它强制归入 reconcile_mismatch 并阻断正常 SOP。 + $hasSummary = data_get($this->meta, 'payment_summary.total_amount') !== null; + $hasReceipt = data_get($this->meta, 'payment_receipts.0.amount') !== null; + if (! $hasSummary && ! $hasReceipt) { + return false; + } + $receiptCents = (int) round(((float) $this->receiptTotal()) * 100); $paidCents = (int) round(((float) ($this->paid_amount ?? 0)) * 100); diff --git a/tests/Feature/AdminPlatformOrderBatchActivateSubscriptionsReconcileMismatchFilterFieldsTest.php b/tests/Feature/AdminPlatformOrderBatchActivateSubscriptionsReconcileMismatchFilterFieldsTest.php index bd53696..4fd6c91 100644 --- a/tests/Feature/AdminPlatformOrderBatchActivateSubscriptionsReconcileMismatchFilterFieldsTest.php +++ b/tests/Feature/AdminPlatformOrderBatchActivateSubscriptionsReconcileMismatchFilterFieldsTest.php @@ -70,6 +70,7 @@ class AdminPlatformOrderBatchActivateSubscriptionsReconcileMismatchFilterFieldsT $page->assertSee('value="1"', false); // 执行批量同步(filtered scope 且必须 syncable_only=1 才能提交) + // 治理优先:当筛选集合包含 reconcile_mismatch=1 时,应触发安全阀阻断同步 $res = $this->post('/admin/platform-orders/batch-activate-subscriptions', [ 'scope' => 'filtered', 'syncable_only' => '1', @@ -78,9 +79,9 @@ class AdminPlatformOrderBatchActivateSubscriptionsReconcileMismatchFilterFieldsT ]); $res->assertRedirect(); + $res->assertSessionHas('warning'); - // 至少验证该订单确实被命中并写入 subscription_activation.subscription_id $order->refresh(); - $this->assertNotEmpty(data_get($order->meta, 'subscription_activation.subscription_id')); + $this->assertEmpty(data_get($order->meta, 'subscription_activation')); } } diff --git a/tests/Feature/AdminPlatformOrderBatchActivateSubscriptionsRefundInconsistentFilterFieldsTest.php b/tests/Feature/AdminPlatformOrderBatchActivateSubscriptionsRefundInconsistentFilterFieldsTest.php index 4ff1abb..054418f 100644 --- a/tests/Feature/AdminPlatformOrderBatchActivateSubscriptionsRefundInconsistentFilterFieldsTest.php +++ b/tests/Feature/AdminPlatformOrderBatchActivateSubscriptionsRefundInconsistentFilterFieldsTest.php @@ -84,20 +84,21 @@ class AdminPlatformOrderBatchActivateSubscriptionsRefundInconsistentFilterFields ]); // 批量同步订阅:filtered scope 必须带 syncable_only=1;并附加 refund_inconsistent=1 - $this->post('/admin/platform-orders/batch-activate-subscriptions', [ + // 治理优先:当筛选集合包含 refund_inconsistent=1 时,应触发安全阀阻断同步 + $res = $this->post('/admin/platform-orders/batch-activate-subscriptions', [ 'scope' => 'filtered', 'syncable_only' => '1', 'refund_inconsistent' => '1', 'limit' => 50, - ])->assertRedirect(); + ]); + $res->assertRedirect(); + $res->assertSessionHas('warning'); $a->refresh(); $b->refresh(); - // A 应被同步(meta.subscription_activation.subscription_id 存在) - $this->assertNotNull(data_get($a->meta, 'subscription_activation.subscription_id')); - - // B 不应被同步 + // 安全阀触发:A/B 都不应被同步 + $this->assertNull(data_get($a->meta, 'subscription_activation.subscription_id')); $this->assertNull(data_get($b->meta, 'subscription_activation.subscription_id')); } }