Files
zyt/server/tests/pharmacy/medicine_bootstrap_mysql_integration.php
T
2026-07-22 14:50:34 +08:00

233 lines
9.4 KiB
PHP

<?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) {
}
}
}