From 19236cb511047223eaeaeab6dcb11556d9529ea0 Mon Sep 17 00:00:00 2001 From: qiuxiang <394405259@qq.com> Date: Tue, 22 Sep 2026 16:46:53 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=AF=E6=8C=81=E4=BA=AC=E4=B8=9C=E6=97=A5?= =?UTF-8?q?=E8=B4=A6=E5=8D=95=E6=88=AA=E6=AD=A2=E6=97=A5=E6=9C=9F=E5=B9=B6?= =?UTF-8?q?=E4=BF=9D=E6=8A=A4=E5=B7=B2=E5=AF=BC=E5=85=A5=E8=8C=83=E5=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/Console/Commands/ImportJdDailyBills.php | 6 ++-- .../Services/JdDailyBillImporter.php | 19 ++++++++++-- .../Unit/EcommerceFinance/JdDailyBillTest.php | 29 +++++++++++++++++-- 3 files changed, 46 insertions(+), 8 deletions(-) diff --git a/app/Console/Commands/ImportJdDailyBills.php b/app/Console/Commands/ImportJdDailyBills.php index 2433acef..3b0ccb3c 100644 --- a/app/Console/Commands/ImportJdDailyBills.php +++ b/app/Console/Commands/ImportJdDailyBills.php @@ -10,8 +10,8 @@ use Throwable; class ImportJdDailyBills extends Command { protected $signature = 'business-mirror:import-jd-daily - {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 ImportJdDailyBills 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/JdDailyBillImporter.php b/modules/EcommerceFinance/Services/JdDailyBillImporter.php index 3a871b5e..7dae4ba9 100644 --- a/modules/EcommerceFinance/Services/JdDailyBillImporter.php +++ b/modules/EcommerceFinance/Services/JdDailyBillImporter.php @@ -24,7 +24,7 @@ class JdDailyBillImporter 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('必须指定完整月份及核对笔数。'); @@ -33,6 +33,13 @@ class JdDailyBillImporter $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 JdDailyBillImporter '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_daily_bill_periods')->updateOrInsert(['period_start' => $start], $state)); + // Publish only the reconciled date range; keep the preceding generation for rollback. + $db->transaction(function () use ($db, $start, $end, $state): void { + $previous = $db->table('jd_daily_bill_periods')->where('period_start', $start)->lockForUpdate()->first(); + if ($previous !== null && $previous->period_end > $end) { + throw new InvalidArgumentException('截止日期早于已导入范围,未替换现有账单。'); + } + $db->table('jd_daily_bill_periods')->updateOrInsert(['period_start' => $start], $state); + }); return $state; } catch (Throwable $e) { $db->table('jd_daily_bill_rows')->where('generation', $generation)->delete(); diff --git a/tests/Unit/EcommerceFinance/JdDailyBillTest.php b/tests/Unit/EcommerceFinance/JdDailyBillTest.php index ee91e713..6aa66dbc 100644 --- a/tests/Unit/EcommerceFinance/JdDailyBillTest.php +++ b/tests/Unit/EcommerceFinance/JdDailyBillTest.php @@ -90,7 +90,32 @@ class JdDailyBillTest extends TestCase app(JdDailyBillRepository::class)->paginate([], 'occurred_at', 'asc', 1, 10); } - private function import(array $rows, int $count, string $income, string $expense): array + public function test_cutoff_validates_rows_and_refuses_to_shorten_existing_coverage(): void + { + $row = array_replace($this->row('123', '10.00'), ['费用结算时间' => '2026-08-21 23:59:59']); + $initial = $this->import([$row], 1, '10.00', '0.00', '2026-08-21'); + $this->assertSame('2026-08-22', $initial['period_end']); + foreach (['2026-08-20', '2026-09-21', '2026-08-32'] as $cutoff) { + try { + $this->import([$row], 1, '10.00', '0.00', $cutoff); + $this->fail('Invalid cutoff accepted'); + } catch (\InvalidArgumentException $e) { + $this->assertSame($initial['active_generation'], DB::connection('business_mirror')->table('jd_daily_bill_periods')->value('active_generation')); + } + } + $full = $this->import([$row, $this->row('124', '2.00')], 2, '12.00', '0.00'); + try { + $this->import([$row], 1, '10.00', '0.00', '2026-08-21'); + $this->fail('Existing later records would be lost'); + } catch (\InvalidArgumentException $e) { + $repo = app(JdDailyBillRepository::class); + $this->assertSame(2, $repo->paginate([], 'occurred_at', 'asc', 1, 10)['total']); + $this->assertSame(2.0, $repo->paginate(['start' => '2026-08-22'], 'occurred_at', 'asc', 1, 10)['summary']['income']); + $this->assertSame($full['active_generation'], DB::connection('business_mirror')->table('jd_daily_bill_periods')->value('active_generation')); + } + } + + 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; @@ -98,7 +123,7 @@ class JdDailyBillTest extends TestCase fputcsv($file, array_keys($rows[0])); foreach ($rows as $row) fputcsv($file, array_values($row)); fclose($file); - return app(JdDailyBillImporter::class)->import($path, '2026-08', $count, $income, $expense); + return app(JdDailyBillImporter::class)->import($path, '2026-08', $count, $income, $expense, $through); } private function row(string $code, string $income, string $expense = '0.00'): array -- 2.47.2