支持进销存多月份权威快照
This commit is contained in:
parent
4782cf62c8
commit
18fdb8a22f
@ -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');
|
||||
}
|
||||
};
|
||||
@ -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
|
||||
{
|
||||
|
||||
@ -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
|
||||
{
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user