From d0abc90033e71422b70236f7e425095b53135c17 Mon Sep 17 00:00:00 2001 From: qiuxiang <394405259@qq.com> Date: Thu, 24 Sep 2026 11:44:59 +0800 Subject: [PATCH] fix: allow verified JD wallet month-to-date imports --- app/Console/Commands/ImportJdWalletBills.php | 6 ++-- .../Services/JdWalletBillImporter.php | 19 ++++++++-- .../EcommerceFinance/JdWalletBillTest.php | 35 +++++++++++++++++-- 3 files changed, 52 insertions(+), 8 deletions(-) diff --git a/app/Console/Commands/ImportJdWalletBills.php b/app/Console/Commands/ImportJdWalletBills.php index 61f7ea44..dae9c717 100644 --- a/app/Console/Commands/ImportJdWalletBills.php +++ b/app/Console/Commands/ImportJdWalletBills.php @@ -10,8 +10,8 @@ use Throwable; class ImportJdWalletBills extends Command { protected $signature = 'business-mirror:import-jd-wallet - {path : 胜算京东钱包完整月份 UTF-8 CSV} {month : YYYY-MM} - {--count= : 核对笔数} {--income= : 核对收入} {--expense= : 核对支出}'; + {path : 胜算京东钱包指定月份月初至截止日期 UTF-8 CSV} {month : YYYY-MM} + {--through= : 截止日期 YYYY-MM-DD,默认月末} {--count= : 核对笔数} {--income= : 核对收入} {--expense= : 核对支出}'; protected $description = '核对并启用胜算京东钱包月份镜像,保留旧版本'; @@ -24,7 +24,7 @@ class ImportJdWalletBills extends Command } try { $result = $importer->import((string) $this->argument('path'), (string) $this->argument('month'), - (int) $this->option('count'), (string) $this->option('income'), (string) $this->option('expense')); + (int) $this->option('count'), (string) $this->option('income'), (string) $this->option('expense'), $this->option('through')); $this->info('京东钱包已启用,笔数:' . $result['row_count']); return self::SUCCESS; } catch (Throwable $e) { diff --git a/modules/EcommerceFinance/Services/JdWalletBillImporter.php b/modules/EcommerceFinance/Services/JdWalletBillImporter.php index 8902030e..b0f9d996 100644 --- a/modules/EcommerceFinance/Services/JdWalletBillImporter.php +++ b/modules/EcommerceFinance/Services/JdWalletBillImporter.php @@ -21,7 +21,7 @@ class JdWalletBillImporter public function __construct(private readonly DatabaseManager $databases) {} - public function import(string $path, string $month, int $expectedCount, string $expectedIncome, string $expectedExpense): array + public function import(string $path, string $month, int $expectedCount, string $expectedIncome, string $expectedExpense, ?string $through = null): array { if (! preg_match('/^\d{4}-(0[1-9]|1[0-2])$/', $month) || $expectedCount < 1) { throw new InvalidArgumentException('必须指定完整月份及核对笔数。'); @@ -30,6 +30,13 @@ class JdWalletBillImporter $expectedOut = $this->cents($expectedExpense); $start = $month . '-01'; $end = Carbon::parse($start)->addMonth()->format('Y-m-d'); + if ($through !== null) { + $date = \DateTimeImmutable::createFromFormat('!Y-m-d', $through); + if (! $date || $date->format('Y-m-d') !== $through || $through < $start || $through >= $end) { + throw new InvalidArgumentException('截止日期必须是指定月份内的有效日期。'); + } + $end = $date->modify('+1 day')->format('Y-m-d'); + } $file = fopen($path, 'rb'); if ($file === false) throw new RuntimeException('无法读取京东钱包明细。'); $db = $this->databases->connection(config('upstream_mirrors.connection', 'business_mirror')); @@ -95,8 +102,14 @@ class JdWalletBillImporter 'row_count' => $count, 'income' => $this->decimal($income), 'expense' => $this->decimal($expense), 'imported_at' => now()->format('Y-m-d H:i:s'), ]; - // Publish only reconciled complete months; keep the preceding generation for rollback. - $db->transaction(fn () => $db->table('jd_wallet_bill_periods')->updateOrInsert(['period_start' => $start], $state)); + // Activate only the reconciled range and retain the previous generation for rollback. + $db->transaction(function () use ($db, $start, $end, $state): void { + $previous = $db->table('jd_wallet_bill_periods')->where('period_start', $start)->lockForUpdate()->first(); + if ($previous !== null && $previous->period_end > $end) { + throw new InvalidArgumentException('截止日期早于已导入范围,未替换现有账单。'); + } + $db->table('jd_wallet_bill_periods')->updateOrInsert(['period_start' => $start], $state); + }); return $state; } catch (Throwable $e) { $db->table('jd_wallet_bill_rows')->where('generation', $generation)->delete(); diff --git a/tests/Unit/EcommerceFinance/JdWalletBillTest.php b/tests/Unit/EcommerceFinance/JdWalletBillTest.php index cf43efe1..e532b837 100644 --- a/tests/Unit/EcommerceFinance/JdWalletBillTest.php +++ b/tests/Unit/EcommerceFinance/JdWalletBillTest.php @@ -83,6 +83,37 @@ class JdWalletBillTest extends TestCase $this->assertSame(12.0, app(StoreReceivingJdWalletBillRepository::class)->paginate([], 'occurred_at', 'asc', 1, 10)['summary']['income']); } + public function test_month_to_date_import_rejects_shorter_replacement_and_out_of_range_rows(): void + { + $row = array_replace($this->row('2000000000000000001', '10.00'), ['费用结算时间' => '2026-08-23 12:00:00']); + $first = $this->import([$row], 1, '10.00', '0.00', '2026-08-23'); + $this->assertSame('2026-08-24', $first['period_end']); + + try { + $earlier = array_replace($row, ['费用结算时间' => '2026-08-22 12:00:00']); + $this->import([$earlier], 1, '10.00', '0.00', '2026-08-22'); + $this->fail('A shorter file must not replace the active range.'); + } catch (\InvalidArgumentException $e) { + $this->assertNotSame('', $e->getMessage()); + } + $this->assertSame($first['active_generation'], DB::connection('business_mirror')->table('jd_wallet_bill_periods')->value('active_generation')); + + $later = array_replace($row, ['记录编码' => '2000000000000000002', '费用结算时间' => '2026-08-24 12:00:00']); + $this->expectException(\InvalidArgumentException::class); + $this->import([$row, $later], 2, '20.00', '0.00', '2026-08-23'); + } + + public function test_month_to_date_import_can_extend_a_verified_range(): void + { + $row = array_replace($this->row('2000000000000000001', '10.00'), ['费用结算时间' => '2026-08-23 12:00:00']); + $first = $this->import([$row], 1, '10.00', '0.00', '2026-08-23'); + $later = array_replace($row, ['记录编码' => '2000000000000000002', '费用结算时间' => '2026-08-24 12:00:00']); + $second = $this->import([$row, $later], 2, '20.00', '0.00', '2026-08-24'); + $this->assertSame('2026-08-25', $second['period_end']); + $this->assertNotSame($first['active_generation'], $second['active_generation']); + $this->assertSame(2, app(StoreReceivingJdWalletBillRepository::class)->paginate(['start' => '2026-08-01', 'end' => '2026-08-24'], 'occurred_at', 'asc', 1, 10)['total']); + } + public function test_uninitialized_mirror_is_not_reported_as_empty_success(): void { $this->expectExceptionMessage('jd_wallet_bill_mirror_not_ready'); @@ -104,7 +135,7 @@ class JdWalletBillTest extends TestCase $this->assertSame(0.0, $repo->paginate(['start' => '2026-09-01'], 'occurred_at', 'desc', 1, 10)['summary']['balance']); } - private function import(array $rows, int $count, string $income, string $expense): array + private function import(array $rows, int $count, string $income, string $expense, ?string $through = null): array { $path = tempnam(sys_get_temp_dir(), 'jd-daily-'); $this->files[] = $path; @@ -112,7 +143,7 @@ class JdWalletBillTest extends TestCase fputcsv($file, array_keys($rows[0])); foreach ($rows as $row) fputcsv($file, array_values($row)); fclose($file); - return app(JdWalletBillImporter::class)->import($path, '2026-08', $count, $income, $expense); + return app(JdWalletBillImporter::class)->import($path, '2026-08', $count, $income, $expense, $through); } private function row(string $code, string $income, string $expense = '0.00'): array -- 2.47.2