feat: add zyt medicine bootstrap
This commit is contained in:
@@ -0,0 +1,232 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require dirname(__DIR__, 2) . '/vendor/autoload.php';
|
||||
|
||||
use app\common\service\pharmacy\EjMedicineBootstrapService;
|
||||
|
||||
$hostname = trim((string) (getenv('ZYT_BOOTSTRAP_TEST_DB_HOST') ?: ''));
|
||||
$port = (int) (getenv('ZYT_BOOTSTRAP_TEST_DB_PORT') ?: 3306);
|
||||
$username = (string) (getenv('ZYT_BOOTSTRAP_TEST_DB_USER') ?: '');
|
||||
$configuredDatabase = trim((string) (getenv('ZYT_BOOTSTRAP_TEST_DB_DATABASE') ?: ''));
|
||||
$passwordOverride = getenv('ZYT_BOOTSTRAP_TEST_DB_PASSWORD');
|
||||
$password = $passwordOverride === false ? '' : $passwordOverride;
|
||||
if ($hostname === '' || $username === '') {
|
||||
fwrite(STDOUT, "medicine bootstrap MySQL integration skipped: database credentials unavailable\n");
|
||||
exit(0);
|
||||
}
|
||||
|
||||
$databaseName = 'zyt_bootstrap_test_' . bin2hex(random_bytes(8));
|
||||
$admin = null;
|
||||
$pdo = null;
|
||||
$temporaryTables = false;
|
||||
$stage = 'connect';
|
||||
try {
|
||||
$admin = new PDO(
|
||||
sprintf('mysql:host=%s;port=%d;charset=utf8mb4', $hostname, $port),
|
||||
$username,
|
||||
$password,
|
||||
[
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
PDO::ATTR_EMULATE_PREPARES => false,
|
||||
]
|
||||
);
|
||||
$stage = 'create_database';
|
||||
try {
|
||||
$admin->exec("CREATE DATABASE `{$databaseName}` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci");
|
||||
} catch (PDOException $exception) {
|
||||
if ($configuredDatabase === '' || !in_array((string) $exception->getCode(), ['42000', '1044'], true)) {
|
||||
throw $exception;
|
||||
}
|
||||
$temporaryTables = true;
|
||||
$databaseName = $configuredDatabase;
|
||||
}
|
||||
$stage = 'test';
|
||||
$pdo = new PDO(
|
||||
sprintf('mysql:host=%s;port=%d;dbname=%s;charset=utf8mb4', $hostname, $port, $databaseName),
|
||||
$username,
|
||||
$password,
|
||||
[
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
PDO::ATTR_EMULATE_PREPARES => false,
|
||||
]
|
||||
);
|
||||
$tableKind = $temporaryTables ? 'CREATE TEMPORARY TABLE' : 'CREATE TABLE';
|
||||
$pdo->exec("{$tableKind} projection_catalog (
|
||||
medicine_code varchar(32) NOT NULL PRIMARY KEY,
|
||||
local_medicine_id bigint unsigned NOT NULL,
|
||||
name varchar(120) NOT NULL
|
||||
) ENGINE=InnoDB");
|
||||
$pdo->exec("{$tableKind} projection_mapping (
|
||||
local_medicine_id bigint unsigned NOT NULL PRIMARY KEY,
|
||||
medicine_code varchar(32) NOT NULL UNIQUE,
|
||||
operator_id bigint unsigned NOT NULL,
|
||||
operator_name varchar(80) NOT NULL
|
||||
) ENGINE=InnoDB");
|
||||
foreach (['submissions', 'callbacks', 'business_links'] as $table) {
|
||||
$pdo->exec("{$tableKind} `{$table}` (id bigint unsigned NOT NULL PRIMARY KEY) ENGINE=InnoDB");
|
||||
}
|
||||
$pdo->exec("INSERT INTO projection_catalog VALUES ('OLD001', 999, '旧投影')");
|
||||
$pdo->exec("INSERT INTO projection_mapping VALUES (999, 'OLD001', 8, 'old-operator')");
|
||||
|
||||
$transaction = static function (callable $operation) use ($pdo): array {
|
||||
$pdo->beginTransaction();
|
||||
try {
|
||||
$result = $operation();
|
||||
$pdo->commit();
|
||||
return $result;
|
||||
} catch (Throwable $exception) {
|
||||
if ($pdo->inTransaction()) {
|
||||
$pdo->rollBack();
|
||||
}
|
||||
throw $exception;
|
||||
}
|
||||
};
|
||||
$referenceCounter = static function () use ($pdo): array {
|
||||
return [
|
||||
'submissions' => (int) $pdo->query('SELECT COUNT(*) FROM submissions')->fetchColumn(),
|
||||
'callbacks' => (int) $pdo->query('SELECT COUNT(*) FROM callbacks')->fetchColumn(),
|
||||
'business_links' => (int) $pdo->query('SELECT COUNT(*) FROM business_links')->fetchColumn(),
|
||||
];
|
||||
};
|
||||
$referenceLocker = static function () use ($pdo): void {
|
||||
foreach (['submissions', 'callbacks', 'business_links'] as $table) {
|
||||
$pdo->query("SELECT id FROM `{$table}` ORDER BY id FOR UPDATE")->fetchAll();
|
||||
}
|
||||
};
|
||||
$locker = static function () use ($pdo): void {
|
||||
$pdo->query('SELECT medicine_code FROM projection_catalog ORDER BY medicine_code FOR UPDATE')->fetchAll();
|
||||
$pdo->query('SELECT local_medicine_id FROM projection_mapping ORDER BY local_medicine_id FOR UPDATE')->fetchAll();
|
||||
};
|
||||
$verifier = static function () use ($pdo): array {
|
||||
return [
|
||||
'catalog' => (int) $pdo->query('SELECT COUNT(*) FROM projection_catalog')->fetchColumn(),
|
||||
'active_mappings' => (int) $pdo->query('SELECT COUNT(*) FROM projection_mapping')->fetchColumn(),
|
||||
'unmapped' => (int) $pdo->query(
|
||||
'SELECT COUNT(*) FROM projection_catalog c LEFT JOIN projection_mapping m '
|
||||
. 'ON m.medicine_code = c.medicine_code WHERE m.local_medicine_id IS NULL'
|
||||
)->fetchColumn(),
|
||||
];
|
||||
};
|
||||
$rows = [
|
||||
['local_medicine_id' => 1, 'medicine_code' => 'EJ000001', 'name' => '黄芪'],
|
||||
['local_medicine_id' => 2, 'medicine_code' => 'EJ000002', 'name' => '党参'],
|
||||
];
|
||||
$replacer = static function (array $nextRows) use ($pdo): void {
|
||||
$pdo->exec('DELETE FROM projection_mapping');
|
||||
$pdo->exec('DELETE FROM projection_catalog');
|
||||
$catalog = $pdo->prepare(
|
||||
'INSERT INTO projection_catalog (medicine_code,local_medicine_id,name) VALUES (?,?,?)'
|
||||
);
|
||||
$mapping = $pdo->prepare(
|
||||
'INSERT INTO projection_mapping (local_medicine_id,medicine_code,operator_id,operator_name) VALUES (?,?,0,?)'
|
||||
);
|
||||
foreach ($nextRows as $row) {
|
||||
$catalog->execute([$row['medicine_code'], $row['local_medicine_id'], $row['name']]);
|
||||
$mapping->execute([$row['local_medicine_id'], $row['medicine_code'], 'system-bootstrap']);
|
||||
}
|
||||
};
|
||||
|
||||
$pdo->exec('INSERT INTO submissions VALUES (1)');
|
||||
try {
|
||||
EjMedicineBootstrapService::replaceProjectionWith(
|
||||
$rows,
|
||||
$transaction,
|
||||
$referenceLocker,
|
||||
$referenceCounter,
|
||||
$locker,
|
||||
$replacer,
|
||||
$verifier
|
||||
);
|
||||
throw new RuntimeException('nonzero MySQL reference gate unexpectedly allowed replacement');
|
||||
} catch (RuntimeException $exception) {
|
||||
if (!str_contains($exception->getMessage(), '业务引用')) {
|
||||
throw $exception;
|
||||
}
|
||||
}
|
||||
$pdo->exec('DELETE FROM submissions');
|
||||
if ((string) $pdo->query('SELECT medicine_code FROM projection_catalog')->fetchColumn() !== 'OLD001') {
|
||||
throw new RuntimeException('nonzero MySQL reference gate mutated the old projection');
|
||||
}
|
||||
|
||||
$result = EjMedicineBootstrapService::replaceProjectionWith(
|
||||
$rows,
|
||||
$transaction,
|
||||
$referenceLocker,
|
||||
$referenceCounter,
|
||||
$locker,
|
||||
$replacer,
|
||||
$verifier
|
||||
);
|
||||
if ($result !== ['catalog' => 2, 'active_mappings' => 2, 'unmapped' => 0]) {
|
||||
throw new RuntimeException('successful MySQL projection replacement did not verify exactly');
|
||||
}
|
||||
$operators = $pdo->query(
|
||||
'SELECT CONCAT(operator_id, ":", operator_name) FROM projection_mapping ORDER BY local_medicine_id'
|
||||
)->fetchAll(PDO::FETCH_COLUMN);
|
||||
if ($operators !== ['0:system-bootstrap', '0:system-bootstrap']) {
|
||||
throw new RuntimeException('bootstrap mappings did not preserve the system operator identity');
|
||||
}
|
||||
|
||||
$pdo->exec('DELETE FROM projection_mapping');
|
||||
$pdo->exec('DELETE FROM projection_catalog');
|
||||
$pdo->exec("INSERT INTO projection_catalog VALUES ('OLD002', 998, '回滚旧投影')");
|
||||
$pdo->exec("INSERT INTO projection_mapping VALUES (998, 'OLD002', 7, 'rollback-operator')");
|
||||
try {
|
||||
EjMedicineBootstrapService::replaceProjectionWith(
|
||||
$rows,
|
||||
$transaction,
|
||||
$referenceLocker,
|
||||
$referenceCounter,
|
||||
$locker,
|
||||
static function (array $nextRows) use ($replacer): void {
|
||||
$replacer($nextRows);
|
||||
throw new RuntimeException('forced MySQL replacement failure');
|
||||
},
|
||||
$verifier
|
||||
);
|
||||
throw new RuntimeException('forced MySQL replacement failure unexpectedly committed');
|
||||
} catch (RuntimeException $exception) {
|
||||
if ($exception->getMessage() !== 'forced MySQL replacement failure') {
|
||||
throw $exception;
|
||||
}
|
||||
}
|
||||
$rolledBack = $pdo->query(
|
||||
'SELECT c.medicine_code,c.local_medicine_id,c.name,m.operator_id,m.operator_name '
|
||||
. 'FROM projection_catalog c JOIN projection_mapping m USING (medicine_code)'
|
||||
)->fetch();
|
||||
if ($rolledBack !== [
|
||||
'medicine_code' => 'OLD002',
|
||||
'local_medicine_id' => 998,
|
||||
'name' => '回滚旧投影',
|
||||
'operator_id' => 7,
|
||||
'operator_name' => 'rollback-operator',
|
||||
]) {
|
||||
throw new RuntimeException('MySQL rollback did not restore the old projection exactly');
|
||||
}
|
||||
|
||||
fwrite(STDOUT, $temporaryTables
|
||||
? "medicine bootstrap MySQL integration passed: temporary_tables\n"
|
||||
: "medicine bootstrap MySQL integration passed: temporary_database\n");
|
||||
} catch (PDOException $exception) {
|
||||
if ($stage === 'test') {
|
||||
throw $exception;
|
||||
}
|
||||
fwrite(STDOUT, sprintf(
|
||||
"medicine bootstrap MySQL integration skipped: temporary database unavailable stage=%s sqlstate=%s\n",
|
||||
$stage,
|
||||
(string) $exception->getCode()
|
||||
));
|
||||
exit(0);
|
||||
} finally {
|
||||
$pdo = null;
|
||||
if ($admin instanceof PDO && !$temporaryTables) {
|
||||
try {
|
||||
$admin->exec("DROP DATABASE IF EXISTS `{$databaseName}`");
|
||||
} catch (Throwable) {
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user