生产业务迁移兼容热修复 #5

Merged
WuYongJie merged 1 commits from hotfix/WuYongJie-business-migration-compatibility into main 2026-08-04 14:23:07 +08:00
3 changed files with 185 additions and 13 deletions

View File

@ -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)
));
}
}
};

View File

@ -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)
));
}
}
};

View 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();
});
}
}