生产业务迁移兼容热修复 #5
@ -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)
|
||||
));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -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)
|
||||
));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
126
tests/Feature/Business/BusinessMigrationCompatibilityTest.php
Normal file
126
tests/Feature/Business/BusinessMigrationCompatibilityTest.php
Normal file
@ -0,0 +1,126 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Business;
|
||||
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use RuntimeException;
|
||||
use Tests\TestCase;
|
||||
|
||||
class BusinessMigrationCompatibilityTest extends TestCase
|
||||
{
|
||||
private const CONNECTION = 'business_migration_compatibility';
|
||||
|
||||
private string $previousDefaultConnection;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->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();
|
||||
});
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user