diff --git a/database/migrations/2026_07_21_000000_create_business_finance_tables.php b/database/migrations/2026_07_21_000000_create_business_finance_tables.php index b44bac7c..85edb3e6 100644 --- a/database/migrations/2026_07_21_000000_create_business_finance_tables.php +++ b/database/migrations/2026_07_21_000000_create_business_finance_tables.php @@ -9,7 +9,7 @@ return new class extends Migration public function up(): void { // 1. 店铺后台税务数据 - Schema::create('ak_biz_tax_data', function (Blueprint $table) { + $this->createOrValidate('ak_biz_tax_data', ['id', 'company_id', 'company_name', 'real_ship_revenue', 'final_tax_revenue', 'period', 'created_at', 'updated_at'], function (Blueprint $table) { $table->id(); $table->foreignId('company_id')->constrained('companies')->cascadeOnDelete(); $table->string('company_name')->default('')->comment('公司名称'); @@ -27,7 +27,7 @@ return new class extends Migration }); // 2. 财务SW规划表 - Schema::create('ak_biz_sw_planning', function (Blueprint $table) { + $this->createOrValidate('ak_biz_sw_planning', ['id', 'company_id', 'company_name', 'final_tax_revenue', 'profit', 'period', 'created_at', 'updated_at'], function (Blueprint $table) { $table->id(); $table->foreignId('company_id')->constrained('companies')->cascadeOnDelete(); $table->string('company_name')->default(''); @@ -43,7 +43,7 @@ return new class extends Migration }); // 3. 实际SW统计表(多级表头,扁平化存储) - Schema::create('biz_sw_actual', function (Blueprint $table) { + $this->createOrValidate('biz_sw_actual', ['id', 'company_id', 'region', 'company_name', 'total_revenue', 'tax_burden_rate', 'created_at', 'updated_at'], function (Blueprint $table) { $table->id(); $table->foreignId('company_id')->constrained('companies')->cascadeOnDelete(); $table->string('region')->default('')->comment('区域'); @@ -101,7 +101,7 @@ return new class extends Migration }); // 4. 公司店铺数据 - Schema::create('biz_company_stores', function (Blueprint $table) { + $this->createOrValidate('biz_company_stores', ['id', 'company_id', 'company_name', 'store_name', 'store_revenue', 'company_revenue', 'period', 'created_at', 'updated_at'], function (Blueprint $table) { $table->id(); $table->foreignId('company_id')->constrained('companies')->cascadeOnDelete(); $table->string('company_name')->default('')->comment('公司名称'); @@ -115,7 +115,7 @@ return new class extends Migration }); // 5. 店铺变更统计表 - Schema::create('ak_biz_store_changes', function (Blueprint $table) { + $this->createOrValidate('ak_biz_store_changes', ['id', 'company_id', 'store_name', 'change_date', 'previous_entity', 'created_at', 'updated_at'], function (Blueprint $table) { $table->id(); $table->foreignId('company_id')->constrained('companies')->cascadeOnDelete(); $table->string('store_name')->default('')->comment('店铺名称'); @@ -127,7 +127,7 @@ return new class extends Migration }); // 6. 发票基础信息+发票数据报表 - Schema::create('ak_biz_invoice_basic', function (Blueprint $table) { + $this->createOrValidate('ak_biz_invoice_basic', ['id', 'company_id', 'store_name', 'invoice_issuer', 'invoice_limit', 'period', 'created_at', 'updated_at'], function (Blueprint $table) { $table->id(); $table->foreignId('company_id')->constrained('companies')->cascadeOnDelete(); $table->string('store_name')->default('')->comment('店铺名称'); @@ -141,7 +141,7 @@ return new class extends Migration }); // 7. 店铺后台开票 - Schema::create('ak_biz_store_invoice', function (Blueprint $table) { + $this->createOrValidate('ak_biz_store_invoice', ['id', 'company_id', 'store', 'seller_name', 'invoice_amount', 'invoice_type', 'invoice_date', 'created_at', 'updated_at'], function (Blueprint $table) { $table->id(); $table->foreignId('company_id')->constrained('companies')->cascadeOnDelete(); $table->string('store')->default('')->comment('店铺'); @@ -157,7 +157,7 @@ return new class extends Migration }); // 8. 资金预测表(月度资金收支预算表) - Schema::create('ak_biz_fund_forecast', function (Blueprint $table) { + $this->createOrValidate('ak_biz_fund_forecast', ['id', 'company_id', 'period', 'actual_period', 'income_total_budget', 'expense_total_actual', 'net_balance_actual', 'created_at', 'updated_at'], function (Blueprint $table) { $table->id(); $table->foreignId('company_id')->constrained('companies')->cascadeOnDelete(); $table->string('period')->default('')->comment('预算填报月份'); @@ -211,7 +211,7 @@ return new class extends Migration }); // 9. 店铺收款账户信息 - Schema::create('ak_biz_store_receiving_accounts', function (Blueprint $table) { + $this->createOrValidate('ak_biz_store_receiving_accounts', ['id', 'company_id', 'platform', 'store_name', 'wallet_no', 'account_number', 'bank_name', 'created_at', 'updated_at'], function (Blueprint $table) { $table->id(); $table->foreignId('company_id')->constrained('companies')->cascadeOnDelete(); $table->string('platform')->default('')->comment('店铺平台'); @@ -228,7 +228,7 @@ return new class extends Migration }); // 10. 银行账户信息 - Schema::create('ak_biz_bank_accounts', function (Blueprint $table) { + $this->createOrValidate('ak_biz_bank_accounts', ['id', 'company_id', 'account_name', 'account_type', 'bank_account', 'bank_name', 'auditor_login_password', 'created_at', 'updated_at'], function (Blueprint $table) { $table->id(); $table->foreignId('company_id')->constrained('companies')->cascadeOnDelete(); $table->string('account_name')->default('')->comment('账户名称'); @@ -243,7 +243,7 @@ return new class extends Migration }); // 11. 快递收款系统 - Schema::create('ak_biz_express_receiving', function (Blueprint $table) { + $this->createOrValidate('ak_biz_express_receiving', ['id', 'company_id', 'express_name', 'receiving_type', 'receiving_name', 'account_number', 'bank_name', 'created_at', 'updated_at'], function (Blueprint $table) { $table->id(); $table->foreignId('company_id')->constrained('companies')->cascadeOnDelete(); $table->string('express_name')->default('')->comment('快递名称'); @@ -273,4 +273,28 @@ return new class extends Migration Schema::dropIfExists($table); } } + + /** + * A failed deployment can leave a created table behind before Laravel writes + * the migration ledger entry. Resume only after proving the existing table + * is structurally compatible with the intended business schema. + */ + private function createOrValidate(string $table, array $requiredColumns, Closure $definition): void + { + if (!Schema::hasTable($table)) { + Schema::create($table, $definition); + + return; + } + + $missingColumns = array_values(array_diff($requiredColumns, Schema::getColumnListing($table))); + + if ($missingColumns !== []) { + throw new RuntimeException(sprintf( + 'Cannot resume migration: existing table "%s" is missing required columns: %s.', + $table, + implode(', ', $missingColumns) + )); + } + } }; diff --git a/database/migrations/2026_07_22_000000_create_business_basic_information_tables.php b/database/migrations/2026_07_22_000000_create_business_basic_information_tables.php index 4a5e99f0..aa2c8161 100644 --- a/database/migrations/2026_07_22_000000_create_business_basic_information_tables.php +++ b/database/migrations/2026_07_22_000000_create_business_basic_information_tables.php @@ -8,7 +8,7 @@ return new class extends Migration { public function up(): void { - Schema::create('business_company_information', function (Blueprint $table) { + $this->createOrValidate('business_company_information', ['id', 'company_id', 'unified_social_credit_code', 'company_name', 'business_scope', 'shareholders', 'created_at', 'updated_at', 'deleted_at'], function (Blueprint $table) { $table->increments('id'); $table->integer('company_id')->default(1)->index(); $table->string('unified_social_credit_code')->nullable()->comment('统一社会信用代码'); @@ -29,7 +29,7 @@ return new class extends Migration $table->softDeletes(); }); - Schema::create('business_store_information', function (Blueprint $table) { + $this->createOrValidate('business_store_information', ['id', 'company_id', 'opened_at', 'erp_store_name', 'platform', 'channel_mode', 'created_at', 'updated_at', 'deleted_at'], function (Blueprint $table) { $table->increments('id'); $table->integer('company_id')->default(1)->index(); $table->date('opened_at')->nullable()->comment('开店时间'); @@ -54,4 +54,26 @@ return new class extends Migration Schema::dropIfExists('business_store_information'); Schema::dropIfExists('business_company_information'); } + + /** + * Let a previously interrupted release continue only from compatible tables. + */ + private function createOrValidate(string $table, array $requiredColumns, Closure $definition): void + { + if (!Schema::hasTable($table)) { + Schema::create($table, $definition); + + return; + } + + $missingColumns = array_values(array_diff($requiredColumns, Schema::getColumnListing($table))); + + if ($missingColumns !== []) { + throw new RuntimeException(sprintf( + 'Cannot resume migration: existing table "%s" is missing required columns: %s.', + $table, + implode(', ', $missingColumns) + )); + } + } }; diff --git a/tests/Feature/Business/BusinessMigrationCompatibilityTest.php b/tests/Feature/Business/BusinessMigrationCompatibilityTest.php new file mode 100644 index 00000000..02afafde --- /dev/null +++ b/tests/Feature/Business/BusinessMigrationCompatibilityTest.php @@ -0,0 +1,126 @@ +previousDefaultConnection = DB::getDefaultConnection(); + + config([ + 'database.connections.' . self::CONNECTION => [ + 'driver' => 'sqlite', + 'database' => ':memory:', + 'prefix' => 'ak_', + 'foreign_key_constraints' => false, + ], + ]); + + DB::purge(self::CONNECTION); + DB::setDefaultConnection(self::CONNECTION); + DB::connection(self::CONNECTION)->getPdo(); + } + + protected function tearDown(): void + { + DB::disconnect(self::CONNECTION); + DB::setDefaultConnection($this->previousDefaultConnection); + + parent::tearDown(); + } + + public function testFinanceMigrationResumesAfterTheFirstTableWasCreatedBeforeTheLedgerEntry(): void + { + $this->createCompaniesTable(); + $this->createCompatibleTaxDataTable(); + + $migration = require database_path('migrations/2026_07_21_000000_create_business_finance_tables.php'); + $migration->up(); + $migration->up(); + + foreach ([ + 'ak_biz_tax_data', + 'ak_biz_sw_planning', + 'biz_sw_actual', + 'biz_company_stores', + 'ak_biz_store_changes', + 'ak_biz_invoice_basic', + 'ak_biz_store_invoice', + 'ak_biz_fund_forecast', + 'ak_biz_store_receiving_accounts', + 'ak_biz_bank_accounts', + 'ak_biz_express_receiving', + ] as $table) { + $this->assertTrue(Schema::hasTable($table), "Expected {$table} to exist after resuming the migration."); + } + } + + public function testFinanceMigrationRejectsAnIncompatiblePartialTable(): void + { + $this->createCompaniesTable(); + Schema::create('ak_biz_tax_data', function (Blueprint $table) { + $table->id(); + }); + + $migration = require database_path('migrations/2026_07_21_000000_create_business_finance_tables.php'); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('ak_biz_tax_data'); + + $migration->up(); + } + + public function testBasicInformationMigrationResumesAfterItsFirstTableWasCreated(): void + { + Schema::create('business_company_information', function (Blueprint $table) { + $table->increments('id'); + $table->integer('company_id'); + $table->string('unified_social_credit_code')->nullable(); + $table->string('company_name')->nullable(); + $table->text('business_scope')->nullable(); + $table->text('shareholders')->nullable(); + $table->timestamps(); + $table->softDeletes(); + }); + + $migration = require database_path('migrations/2026_07_22_000000_create_business_basic_information_tables.php'); + $migration->up(); + $migration->up(); + + $this->assertTrue(Schema::hasTable('business_company_information')); + $this->assertTrue(Schema::hasTable('business_store_information')); + } + + private function createCompaniesTable(): void + { + Schema::create('companies', function (Blueprint $table) { + $table->id(); + }); + } + + private function createCompatibleTaxDataTable(): void + { + Schema::create('ak_biz_tax_data', function (Blueprint $table) { + $table->id(); + $table->foreignId('company_id'); + $table->string('company_name')->default(''); + $table->decimal('real_ship_revenue', 18, 2)->default(0); + $table->decimal('final_tax_revenue', 18, 2)->default(0); + $table->string('period')->default(''); + $table->timestamps(); + }); + } +}