修复支付宝账单翻页重复全表计数 #131

Merged
WuYongJie merged 1 commits from fix/WuYongJie-alipay-pagination-count into main 2026-09-08 21:46:34 +08:00
2 changed files with 41 additions and 2 deletions

View File

@ -40,7 +40,14 @@ class StoreReceivingAlipayBillMirrorRepository
$query = $this->billQuery();
$this->applyFilters($query, $filters);
$total = (clone $query)->count();
$state = $this->mirror->state($dataset) ?? [];
// The synchronizer persists an exact COUNT in the same transaction that
// marks a run successful. Never substitute a database row estimate.
$hasFilters = collect($filters)->contains(static fn (mixed $value): bool => $value !== null && trim((string) $value) !== '');
$recordedCount = $state['mirror_row_count'] ?? null;
$useRecordedCount = ! $hasFilters && ($state['status'] ?? null) === 'succeeded'
&& $recordedCount !== null && ctype_digit((string) $recordedCount);
$total = $useRecordedCount ? (int) $recordedCount : (clone $query)->count();
$summary = $this->summary($query, $filters);
$billRows = $query
@ -54,7 +61,6 @@ class StoreReceivingAlipayBillMirrorRepository
$rows = collect($billRows)
->map(fn (object $row): array => $this->mapRow($row, $shopNames[(string) $row->shop_id] ?? null))
->all();
$state = $this->mirror->state($dataset) ?? [];
$income = $summary['income'];
$expense = $summary['expense'];

View File

@ -43,6 +43,7 @@ class StoreReceivingAlipayBillMirrorRepositoryTest extends TestCase
$table->dateTime('initial_completed_at')->nullable();
$table->string('status')->nullable();
$table->dateTime('last_succeeded_at')->nullable();
$table->unsignedBigInteger('mirror_row_count')->nullable();
});
$schema->create('ops_jst_original_bill_info', function (Blueprint $table): void {
$table->integer('Id');
@ -160,6 +161,38 @@ class StoreReceivingAlipayBillMirrorRepositoryTest extends TestCase
$this->assertSame(['income' => 100.25, 'expense' => 20.10, 'balance' => 80.15, 'count' => 1], $result['summary']);
}
public function test_unfiltered_pages_reuse_exact_completed_count_and_observe_the_next_sync(): void
{
$this->markReady();
$db = DB::connection('business_mirror');
$db->table('ops_jst_original_bill_info')->insert([
$this->row(1, 7, '2026-08-01 09:00:00', '1', 10),
$this->row(2, 7, '2026-08-02 09:00:00', '-1', 5),
]);
$state = $db->table('business_mirror_table_states')->where('dataset_key', StoreReceivingAlipayBillMirrorRepository::DATASET);
$state->update(['mirror_row_count' => 2]);
$db->enableQueryLog();
$first = $this->repository->paginate([], 'occurred_at', 'desc', 1, 1);
$second = $this->repository->paginate(['store_name' => '', 'start' => null], 'occurred_at', 'desc', 2, 1);
$this->assertSame(2, $first['total']);
$this->assertSame(2, $second['total']);
$this->assertNotSame($first['data'][0]['id'], $second['data'][0]['id']);
$this->assertCount(0, array_filter($db->getQueryLog(), fn ($q) => str_contains(strtolower($q['query']), 'count(*)')));
$db->disableQueryLog();
$db->table('ops_jst_original_bill_info')->insert($this->row(3, 7, '2026-08-03 09:00:00', '1', 4));
$state->update(['mirror_row_count' => 3, 'last_succeeded_at' => '2026-09-08 09:00:00']);
$this->assertSame(3, $this->repository->paginate([], 'occurred_at', 'desc', 1, 1)['total']);
$this->assertSame(1, $this->repository->paginate(['business_type' => '支出'], 'occurred_at', 'desc', 1, 1)['total']);
$state->update(['status' => 'running', 'mirror_row_count' => 2]);
$this->assertSame(3, $this->repository->paginate([], 'occurred_at', 'desc', 1, 1)['total']);
$state->update(['status' => 'failed']);
$this->assertSame(3, $this->repository->paginate([], 'occurred_at', 'desc', 1, 1)['total']);
$state->update(['status' => 'succeeded', 'mirror_row_count' => null]);
$this->assertSame(3, $this->repository->paginate([], 'occurred_at', 'desc', 1, 1)['total']);
}
private function markReady(): void
{
foreach ([StoreReceivingAlipayBillMirrorRepository::DATASET, StoreReceivingAlipayBillMirrorRepository::SHOP_DATASET] as $dataset) {