支持进销存多月份权威快照

This commit is contained in:
chenxi 2026-09-26 22:45:37 +08:00
parent 4782cf62c8
commit 18fdb8a22f
4 changed files with 186 additions and 16 deletions

View File

@ -0,0 +1,70 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
private function connection(): string
{
return (string) config('upstream_mirrors.connection', 'business_mirror');
}
public function up(): void
{
$connection = $this->connection();
$schema = Schema::connection($connection);
if (! $schema->hasTable('inventory_report_authoritative_months')) {
$schema->create('inventory_report_authoritative_months', function (Blueprint $table): void {
$table->string('dataset_key', 120);
$table->date('source_month');
$table->string('status', 24)->default('succeeded');
$table->uuid('active_generation')->nullable();
$table->string('source_filename', 255)->nullable();
$table->char('source_sha256', 64)->nullable();
$table->unsignedBigInteger('source_row_count')->default(0);
$table->dateTime('last_succeeded_at')->nullable();
$table->timestamps();
$table->primary(['dataset_key', 'source_month']);
$table->index(
['dataset_key', 'source_month', 'active_generation'],
'inventory_report_authoritative_month_lookup',
);
});
}
// Preserve the already imported single-month snapshot when upgrading
// from the original one-row state table.
if ($schema->hasTable('inventory_report_authoritative_states')) {
$legacy = DB::connection($connection)
->table('inventory_report_authoritative_states')
->where('dataset_key', 'jiansun.inventory-report.authoritative-month')
->whereNotNull('source_month')
->whereNotNull('active_generation')
->first();
if ($legacy !== null) {
DB::connection($connection)->table('inventory_report_authoritative_months')->insertOrIgnore([
'dataset_key' => (string) $legacy->dataset_key,
'source_month' => $legacy->source_month,
'status' => (string) ($legacy->status ?: 'succeeded'),
'active_generation' => $legacy->active_generation,
'source_filename' => $legacy->source_filename,
'source_sha256' => $legacy->source_sha256,
'source_row_count' => (int) ($legacy->source_row_count ?? 0),
'last_succeeded_at' => $legacy->last_succeeded_at,
'created_at' => $legacy->created_at,
'updated_at' => $legacy->updated_at,
]);
}
}
}
public function down(): void
{
Schema::connection($this->connection())->dropIfExists('inventory_report_authoritative_months');
}
};

View File

@ -726,17 +726,13 @@ class ThirdPartyWarehouseStorageMirrorRepository
}
$month = Carbon::parse($period['start'])->startOfMonth()->toDateString();
$state = $this->connection->table(self::INVENTORY_REPORT_AUTHORITATIVE_STATE_TABLE)
->where('dataset_key', self::INVENTORY_REPORT_AUTHORITATIVE_DATASET_KEY)
->whereNotNull('active_generation')
->where('source_month', $month)
->first();
if ($state === null || trim((string) $state->active_generation) === '') {
$authority = $this->currentMonthlyInventoryAuthority($period);
if ($authority === null) {
return null;
}
$query = $this->connection->table(self::INVENTORY_REPORT_AUTHORITATIVE_ROW_TABLE)
->where('generation', (string) $state->active_generation)
->where('generation', $authority['generation'])
->where('report_month', $month)
->select([
'owner', 'report_month', 'warehouse_provider', 'warehouse',
@ -1267,11 +1263,16 @@ class ThirdPartyWarehouseStorageMirrorRepository
private function currentMonthlyInventoryAuthority(array $period): ?array
{
$month = Carbon::parse($period['start'])->startOfMonth();
$state = $this->connection->table(self::INVENTORY_REPORT_AUTHORITATIVE_STATE_TABLE)
->where('dataset_key', self::INVENTORY_REPORT_AUTHORITATIVE_DATASET_KEY)
->whereNotNull('active_generation')
->whereDate('source_month', '=', $month->toDateString())
$state = $this->monthlyAuthorityStateQuery()
?->whereDate('source_month', '=', $month->toDateString())
->first();
if ($state === null) {
$state = $this->connection->table(self::INVENTORY_REPORT_AUTHORITATIVE_STATE_TABLE)
->where('dataset_key', self::INVENTORY_REPORT_AUTHORITATIVE_DATASET_KEY)
->whereNotNull('active_generation')
->whereDate('source_month', '=', $month->toDateString())
->first();
}
if ($state === null || trim((string) $state->active_generation) === '') {
return null;
}
@ -1285,12 +1286,19 @@ class ThirdPartyWarehouseStorageMirrorRepository
/** @param array{start:string,end:string} $period */
private function previousMonthlyInventoryAuthority(array $period): ?array
{
$state = $this->connection->table(self::INVENTORY_REPORT_AUTHORITATIVE_STATE_TABLE)
->where('dataset_key', self::INVENTORY_REPORT_AUTHORITATIVE_DATASET_KEY)
->whereNotNull('active_generation')
->whereDate('source_month', '<', Carbon::parse($period['start'])->startOfMonth())
$month = Carbon::parse($period['start'])->startOfMonth();
$state = $this->monthlyAuthorityStateQuery()
?->whereDate('source_month', '<', $month)
->orderByDesc('source_month')
->first();
if ($state === null) {
$state = $this->connection->table(self::INVENTORY_REPORT_AUTHORITATIVE_STATE_TABLE)
->where('dataset_key', self::INVENTORY_REPORT_AUTHORITATIVE_DATASET_KEY)
->whereNotNull('active_generation')
->whereDate('source_month', '<', $month)
->orderByDesc('source_month')
->first();
}
if ($state === null || trim((string) $state->active_generation) === '') {
return null;
}
@ -1301,6 +1309,23 @@ class ThirdPartyWarehouseStorageMirrorRepository
];
}
private function monthlyAuthorityStateQuery(): ?Builder
{
try {
$schema = Schema::connection($this->connection->getName());
if (! $schema->hasTable('inventory_report_authoritative_months')) {
return null;
}
return $this->connection->table('inventory_report_authoritative_months')
->where('dataset_key', self::INVENTORY_REPORT_AUTHORITATIVE_DATASET_KEY)
->where('status', 'succeeded')
->whereNotNull('active_generation');
} catch (Throwable) {
return null;
}
}
/** @return array{0:string,1:list<mixed>} */
private function subwarehouseIdSql(string $providerColumn, string $warehouseColumn): array
{

View File

@ -24,6 +24,8 @@ class InventoryReportAuthoritativeImporter
private const ROW_TABLE = 'inventory_report_authoritative_rows';
private const MONTH_TABLE = 'inventory_report_authoritative_months';
/** @var list<string> */
private const REQUIRED_HEADERS = [
'货主', '月份', '仓储方', '仓库', '商品编码', '分类', '期初数量', '期末数量',
@ -84,6 +86,23 @@ class InventoryReportAuthoritativeImporter
$source,
$now,
): void {
if ($this->hasMonthlyStateTable()) {
$this->connection->table(self::MONTH_TABLE)->updateOrInsert(
[
'dataset_key' => self::DATASET_KEY,
'source_month' => $source['source_month'],
],
[
'status' => 'succeeded',
'active_generation' => $generation,
'source_filename' => mb_substr(basename($path), 0, 255),
'source_sha256' => $hash,
'source_row_count' => $source['source_rows'],
'last_succeeded_at' => $now,
'updated_at' => $now,
],
);
}
$this->connection->table(self::STATE_TABLE)
->where('dataset_key', self::DATASET_KEY)
->update([
@ -116,8 +135,21 @@ class InventoryReportAuthoritativeImporter
}
try {
$keepGenerations = [$generation];
if ($this->hasMonthlyStateTable()) {
$keepGenerations = array_merge(
$keepGenerations,
$this->connection->table(self::MONTH_TABLE)
->where('dataset_key', self::DATASET_KEY)
->whereNotNull('active_generation')
->pluck('active_generation')
->map(static fn (mixed $value): string => (string) $value)
->all(),
);
}
$keepGenerations = array_values(array_unique(array_filter($keepGenerations)));
$this->connection->table(self::ROW_TABLE)
->where('generation', '!=', $generation)
->whereNotIn('generation', $keepGenerations)
->delete();
} catch (Throwable $exception) {
report($exception);
@ -154,6 +186,11 @@ class InventoryReportAuthoritativeImporter
}
}
private function hasMonthlyStateTable(): bool
{
return Schema::connection($this->connection->getName())->hasTable(self::MONTH_TABLE);
}
/** @return array{source_rows:int,source_month:string} */
private function readAndStore(string $path, string $generation, mixed $importedAt): array
{

View File

@ -69,6 +69,32 @@ class InventoryReportAuthoritativeImportTest extends TestCase
], 'business_mirror');
}
public function test_it_keeps_authoritative_generations_for_multiple_months(): void
{
$august = app(InventoryReportAuthoritativeImporter::class)->import($this->workbook([
$this->headers(),
$this->row('2026-08-01', '仓储方', '主仓', 'SKU-A', 10, 12, ['采购进仓' => 2]),
]));
$july = app(InventoryReportAuthoritativeImporter::class)->import($this->workbook([
$this->headers(),
$this->row('2026-07-01', '仓储方', '主仓', 'SKU-A', 8, 10, ['采购进仓' => 2]),
]));
$months = DB::connection('business_mirror')
->table('inventory_report_authoritative_months')
->where('dataset_key', InventoryReportAuthoritativeImporter::DATASET_KEY)
->orderBy('source_month')
->get();
$this->assertCount(2, $months);
$this->assertSame('2026-07-01', (string) $months[0]->source_month);
$this->assertSame($july['generation'], (string) $months[0]->active_generation);
$this->assertSame('2026-08-01', (string) $months[1]->source_month);
$this->assertSame($august['generation'], (string) $months[1]->active_generation);
$this->assertSame(2, DB::connection('business_mirror')
->table('inventory_report_authoritative_rows')->count());
}
public function test_invalid_replacement_keeps_previous_generation(): void
{
$valid = app(InventoryReportAuthoritativeImporter::class)->import($this->workbook([
@ -158,6 +184,18 @@ class InventoryReportAuthoritativeImportTest extends TestCase
$table->dateTime('imported_at');
$table->primary(['generation', 'row_key']);
});
$schema->create('inventory_report_authoritative_months', function (Blueprint $table): void {
$table->string('dataset_key');
$table->date('source_month');
$table->string('status')->default('succeeded');
$table->string('active_generation')->nullable();
$table->string('source_filename')->nullable();
$table->string('source_sha256')->nullable();
$table->unsignedBigInteger('source_row_count')->default(0);
$table->dateTime('last_succeeded_at')->nullable();
$table->timestamps();
$table->primary(['dataset_key', 'source_month']);
});
}
private function headers(): array