支持京东账单月中截止日期并保护后续账单 #241
@ -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) {
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user