Fix reconcile mismatch predicate and align batch activate governance tests

This commit is contained in:
萝卜
2026-03-13 11:46:00 +00:00
parent ffbf68679b
commit f7c67376f2
4 changed files with 26 additions and 10 deletions

View File

@@ -1385,7 +1385,11 @@ class PlatformOrderController extends Controller
if ($driver === 'sqlite') { if ($driver === 'sqlite') {
// sqlite 下 JSON_EXTRACT 直接返回标量(数值或字符串),这里用“按分”取整避免浮点误差导致 0.01 边界不稳定 // sqlite 下 JSON_EXTRACT 直接返回标量(数值或字符串),这里用“按分”取整避免浮点误差导致 0.01 边界不稳定
// total_cents = (payment_summary.total_amount 存在 ? summary*100 : sum(payment_receipts[].amount)*100) // 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 { } else {
// MySQL 下 JSON_EXTRACT 返回 JSON需要 JSON_UNQUOTE 再 cast同样按分取整避免浮点误差 // MySQL 下 JSON_EXTRACT 返回 JSON需要 JSON_UNQUOTE 再 cast同样按分取整避免浮点误差
// total_cents = (payment_summary.total_amount 存在 ? summary*100 : SUM(payment_receipts[].amount)*100) // total_cents = (payment_summary.total_amount 存在 ? summary*100 : SUM(payment_receipts[].amount)*100)

View File

@@ -65,7 +65,17 @@ class PlatformOrder extends Model
public function isReconcileMismatch(): bool 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); $receiptCents = (int) round(((float) $this->receiptTotal()) * 100);
$paidCents = (int) round(((float) ($this->paid_amount ?? 0)) * 100); $paidCents = (int) round(((float) ($this->paid_amount ?? 0)) * 100);

View File

@@ -70,6 +70,7 @@ class AdminPlatformOrderBatchActivateSubscriptionsReconcileMismatchFilterFieldsT
$page->assertSee('value="1"', false); $page->assertSee('value="1"', false);
// 执行批量同步filtered scope 且必须 syncable_only=1 才能提交) // 执行批量同步filtered scope 且必须 syncable_only=1 才能提交)
// 治理优先:当筛选集合包含 reconcile_mismatch=1 时,应触发安全阀阻断同步
$res = $this->post('/admin/platform-orders/batch-activate-subscriptions', [ $res = $this->post('/admin/platform-orders/batch-activate-subscriptions', [
'scope' => 'filtered', 'scope' => 'filtered',
'syncable_only' => '1', 'syncable_only' => '1',
@@ -78,9 +79,9 @@ class AdminPlatformOrderBatchActivateSubscriptionsReconcileMismatchFilterFieldsT
]); ]);
$res->assertRedirect(); $res->assertRedirect();
$res->assertSessionHas('warning');
// 至少验证该订单确实被命中并写入 subscription_activation.subscription_id
$order->refresh(); $order->refresh();
$this->assertNotEmpty(data_get($order->meta, 'subscription_activation.subscription_id')); $this->assertEmpty(data_get($order->meta, 'subscription_activation'));
} }
} }

View File

@@ -84,20 +84,21 @@ class AdminPlatformOrderBatchActivateSubscriptionsRefundInconsistentFilterFields
]); ]);
// 批量同步订阅filtered scope 必须带 syncable_only=1并附加 refund_inconsistent=1 // 批量同步订阅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', 'scope' => 'filtered',
'syncable_only' => '1', 'syncable_only' => '1',
'refund_inconsistent' => '1', 'refund_inconsistent' => '1',
'limit' => 50, 'limit' => 50,
])->assertRedirect(); ]);
$res->assertRedirect();
$res->assertSessionHas('warning');
$a->refresh(); $a->refresh();
$b->refresh(); $b->refresh();
// A 应被同步meta.subscription_activation.subscription_id 存在) // 安全阀触发A/B 都不应被同步
$this->assertNotNull(data_get($a->meta, 'subscription_activation.subscription_id')); $this->assertNull(data_get($a->meta, 'subscription_activation.subscription_id'));
// B 不应被同步
$this->assertNull(data_get($b->meta, 'subscription_activation.subscription_id')); $this->assertNull(data_get($b->meta, 'subscription_activation.subscription_id'));
} }
} }