feat(M09-D1): 完成商品目录与库存流水底座
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { existsSync, readFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
|
||||
const root = process.cwd();
|
||||
const read = (path) => readFileSync(join(root, path), 'utf8');
|
||||
|
||||
for (const suffix of ['up', 'down', 'verify']) {
|
||||
assert.ok(
|
||||
existsSync(join(root,
|
||||
`database/migrations/2026081107_m09d1_product_inventory_foundation.${suffix}.sql`)),
|
||||
`M09-D1 ${suffix} migration is missing`
|
||||
);
|
||||
}
|
||||
|
||||
const migration = read('database/migrations/2026081107_m09d1_product_inventory_foundation.up.sql');
|
||||
for (const pattern of [
|
||||
'qipai_product_categories',
|
||||
'qipai_products',
|
||||
'qipai_product_skus',
|
||||
'qipai_product_store_listings',
|
||||
'qipai_product_store_hours',
|
||||
'qipai_product_inventory',
|
||||
'qipai_product_inventory_requests',
|
||||
'qipai_product_inventory_ledger',
|
||||
'delivery_enabled',
|
||||
'storage_enabled',
|
||||
'crosses_midnight',
|
||||
'manual_paused_at',
|
||||
'loss_quantity',
|
||||
'loss_delta',
|
||||
'uq_qipai_product_inventory_request',
|
||||
'ADD COLUMN checksum',
|
||||
'uq_qipai_product_inventory_ledger_request',
|
||||
'uq_qipai_product_inventory_ledger_version',
|
||||
'PRODUCT_INVENTORY_LEDGER_IMMUTABLE',
|
||||
'product.catalog.read',
|
||||
'inventory.adjust'
|
||||
]) assert.ok(migration.includes(pattern), `migration is missing ${pattern}`);
|
||||
assert.match(migration, /sale_price_cents INT UNSIGNED/);
|
||||
assert.match(migration, /available_quantity BIGINT UNSIGNED/);
|
||||
assert.match(migration, /operation IN \([\s\S]*'INBOUND'[\s\S]*'STOCKTAKE'[\s\S]*'LOSS'/);
|
||||
|
||||
const catalog = read('backend/src/products/product-catalog-repository.ts');
|
||||
for (const pattern of [
|
||||
'ProductCatalogRepository',
|
||||
'expectedVersion',
|
||||
'PRODUCT_CATEGORY_VERSION_CONFLICT',
|
||||
'PRODUCT_VERSION_CONFLICT',
|
||||
'PRODUCT_SKU_VERSION_CONFLICT',
|
||||
'PRODUCT_LISTING_VERSION_CONFLICT',
|
||||
'PRODUCT_STORE_SETTINGS_VERSION_CONFLICT',
|
||||
'crossesMidnight',
|
||||
'manualPaused'
|
||||
]) assert.ok(catalog.includes(pattern), `product catalog is missing ${pattern}`);
|
||||
assert.match(catalog, /INSERT INTO qipai_audit_logs/);
|
||||
|
||||
const inventory = read('backend/src/inventory/inventory-service.ts');
|
||||
for (const pattern of [
|
||||
'InventoryService',
|
||||
'lockMany',
|
||||
'releaseMany',
|
||||
'deductMany',
|
||||
'INVENTORY_INSUFFICIENT_AVAILABLE',
|
||||
'INVENTORY_INSUFFICIENT_LOCKED',
|
||||
'INVENTORY_IDEMPOTENCY_CONFLICT',
|
||||
'FOR UPDATE',
|
||||
'qipai_product_inventory_ledger',
|
||||
'qipai_audit_logs'
|
||||
]) assert.ok(inventory.includes(pattern), `inventory service is missing ${pattern}`);
|
||||
|
||||
const productRoutes = read('backend/src/routes/products.ts');
|
||||
const inventoryRoutes = read('backend/src/routes/inventory.ts');
|
||||
assert.match(productRoutes, /product-categories/);
|
||||
assert.match(productRoutes, /\/admin-api\/products/);
|
||||
assert.match(productRoutes, /product-sales-settings/);
|
||||
assert.match(inventoryRoutes, /inventory\/stocks/);
|
||||
assert.match(inventoryRoutes, /\/adjust/);
|
||||
|
||||
const mysqlTest = read('backend/tests/mysql-migration-roundtrip.test.mjs');
|
||||
for (const pattern of [
|
||||
'assertProductInventoryFoundation',
|
||||
'INVENTORY_INSUFFICIENT_AVAILABLE',
|
||||
'PRODUCT_INVENTORY_LEDGER_IMMUTABLE',
|
||||
'concurrent inventory lock',
|
||||
'cross-midnight product hours'
|
||||
]) assert.ok(mysqlTest.includes(pattern), `live MySQL test is missing ${pattern}`);
|
||||
|
||||
console.log('PASS: M09-D1 catalog, store-hours, stock concurrency and immutable-ledger gates are present.');
|
||||
@@ -21,6 +21,10 @@ $requiredFiles = @(
|
||||
"backend/src/routes/health.ts",
|
||||
"backend/src/routes/platform-bootstrap.ts",
|
||||
"backend/src/routes/platform-management.ts",
|
||||
"backend/src/routes/products.ts",
|
||||
"backend/src/routes/inventory.ts",
|
||||
"backend/src/products/product-catalog-repository.ts",
|
||||
"backend/src/inventory/inventory-service.ts",
|
||||
"backend/src/routes/auth.ts",
|
||||
"backend/src/tenancy/platform-config-repository.ts",
|
||||
"backend/src/tenancy/platform-admin-repository.ts",
|
||||
@@ -36,6 +40,10 @@ $requiredFiles = @(
|
||||
"backend/tests/platform-management.test.mjs",
|
||||
"backend/tests/auth.test.mjs",
|
||||
"backend/tests/rbac.test.mjs",
|
||||
"backend/tests/product-catalog.test.mjs",
|
||||
"backend/tests/product-route.test.mjs",
|
||||
"backend/tests/inventory-service.test.mjs",
|
||||
"backend/tests/inventory-route.test.mjs",
|
||||
"scripts/dev/wsl/mysql-migration-roundtrip.sh",
|
||||
"database/migrations/2026061601_m01b_core_schema.up.sql",
|
||||
"database/migrations/2026061601_m01b_core_schema.down.sql",
|
||||
@@ -64,6 +72,9 @@ $requiredFiles = @(
|
||||
"database/migrations/2026062015_m05a_payment_domain.up.sql",
|
||||
"database/migrations/2026062015_m05a_payment_domain.down.sql",
|
||||
"database/migrations/2026062015_m05a_payment_domain.verify.sql",
|
||||
"database/migrations/2026081107_m09d1_product_inventory_foundation.up.sql",
|
||||
"database/migrations/2026081107_m09d1_product_inventory_foundation.down.sql",
|
||||
"database/migrations/2026081107_m09d1_product_inventory_foundation.verify.sql",
|
||||
"database/seeds/2026061601_m01b_minimal_seed.sql",
|
||||
"deploy/pm2/ecosystem.config.cjs"
|
||||
)
|
||||
|
||||
@@ -3,7 +3,7 @@ $ErrorActionPreference = "Stop"
|
||||
$readmePath = "README.md"
|
||||
$configPath = "docs/configuration.md"
|
||||
$baselinePath = "docs/current-baseline.md"
|
||||
$expectedSpec = "V5.6.md"
|
||||
$expectedSpec = "V5.7.md"
|
||||
$expectedOrigin = "ssh://git@git.txyundm.cn:2222/panda/qipai.git"
|
||||
|
||||
foreach ($path in @($readmePath, $configPath, $baselinePath, $expectedSpec)) {
|
||||
@@ -23,9 +23,11 @@ $config = Get-Content -Raw -Encoding UTF8 $configPath
|
||||
$spec = Get-Content -Raw -Encoding UTF8 $expectedSpec
|
||||
$baseline = Get-Content -Raw -Encoding UTF8 $baselinePath
|
||||
|
||||
$recentEngineeringLabel = [regex]::Unescape("\u6700\u8fd1\u5de5\u7a0b\u63d0\u4ea4")
|
||||
$recentEngineeringCommitLabel = [regex]::Unescape("\u6700\u8fd1\u5de5\u7a0b\u63d0\u4ea4")
|
||||
$recentEngineeringResultLabel = [regex]::Unescape("\u6700\u8fd1\u5de5\u7a0b\u7ed3\u679c")
|
||||
$nextEngineeringLabel = [regex]::Unescape("\u4e0b\u4e00\u5de5\u7a0b\u76ee\u6807")
|
||||
$pendingWord = [regex]::Unescape("\u5f85")
|
||||
$currentEngineeringPhrase = [regex]::Unescape("\u968f\u672c\u6b21\u4e2d\u6587\u5de5\u7a0b\u63d0\u4ea4\u56fa\u5316")
|
||||
|
||||
$requiredSharedValues = @(
|
||||
$expectedSpec,
|
||||
@@ -59,7 +61,9 @@ foreach ($value in @($expectedSpec, $expectedOrigin, "admin123", "root123", "101
|
||||
}
|
||||
|
||||
$readmeLines = Get-Content -Encoding UTF8 $readmePath
|
||||
$recentLine = $readmeLines | Where-Object { $_.Contains($recentEngineeringLabel) } | Select-Object -First 1
|
||||
$recentLine = $readmeLines | Where-Object {
|
||||
$_.Contains($recentEngineeringCommitLabel) -or $_.Contains($recentEngineeringResultLabel)
|
||||
} | Select-Object -Last 1
|
||||
$nextLine = $readmeLines | Where-Object { $_.Contains($nextEngineeringLabel) } | Select-Object -First 1
|
||||
if (-not $recentLine) {
|
||||
throw "README missing recent engineering commit line"
|
||||
@@ -87,8 +91,8 @@ if (-not $latestEngineeringCommit) {
|
||||
throw "Could not find a recent engineering commit"
|
||||
}
|
||||
|
||||
if ($readme -notmatch [regex]::Escape($latestEngineeringCommit)) {
|
||||
throw "README recent engineering commit must reference latest engineering commit $latestEngineeringCommit"
|
||||
if ($readme -notmatch [regex]::Escape($latestEngineeringCommit) -and -not $readme.Contains($currentEngineeringPhrase)) {
|
||||
throw "README recent engineering result must reference $latestEngineeringCommit or identify the current staged engineering commit"
|
||||
}
|
||||
|
||||
if ($baseline -notmatch '(?m)^>\s*audited_commit:\s*`?[0-9a-f]{7,40}`?') {
|
||||
|
||||
@@ -38,6 +38,8 @@ node scripts/check-m09-b-cleaning-rules.mjs
|
||||
Assert-NativeSuccess "check-m09-b-cleaning-rules"
|
||||
node scripts/check-m09-c-settlement-integrity.mjs
|
||||
Assert-NativeSuccess "check-m09-c-settlement-integrity"
|
||||
node scripts/check-m09-d1-product-inventory.mjs
|
||||
Assert-NativeSuccess "check-m09-d1-product-inventory"
|
||||
|
||||
if (Test-Path "admin/package.json") {
|
||||
Push-Location admin
|
||||
|
||||
@@ -37,6 +37,8 @@ database="qipai_m01c_test_${run_id}"
|
||||
username="qmb_$$_$(openssl rand -hex 4)"
|
||||
password="$(openssl rand -hex 24)"
|
||||
created=0
|
||||
trust_function_creators_before="$(mysql --batch --skip-column-names -e 'SELECT @@GLOBAL.log_bin_trust_function_creators')"
|
||||
trust_function_creators_changed=0
|
||||
|
||||
cleanup() {
|
||||
if [[ "${created}" == "1" ]]; then
|
||||
@@ -45,6 +47,11 @@ DROP DATABASE IF EXISTS \`${database}\`;
|
||||
DROP USER IF EXISTS '${username}'@'127.0.0.1';
|
||||
SQL
|
||||
fi
|
||||
if [[ "${trust_function_creators_changed}" == "1" ]]; then
|
||||
mysql --batch --skip-column-names \
|
||||
-e "SET GLOBAL log_bin_trust_function_creators = ${trust_function_creators_before}" >/dev/null
|
||||
trust_function_creators_changed=0
|
||||
fi
|
||||
}
|
||||
trap cleanup EXIT INT TERM
|
||||
|
||||
@@ -55,6 +62,11 @@ CREATE DATABASE \`${database}\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci
|
||||
CREATE USER '${username}'@'127.0.0.1' IDENTIFIED BY '${password}';
|
||||
GRANT ALL PRIVILEGES ON \`${database}\`.* TO '${username}'@'127.0.0.1';
|
||||
SQL
|
||||
if [[ "${trust_function_creators_before}" != "1" ]]; then
|
||||
mysql --batch --skip-column-names \
|
||||
-e 'SET GLOBAL log_bin_trust_function_creators = 1' >/dev/null
|
||||
trust_function_creators_changed=1
|
||||
fi
|
||||
}
|
||||
|
||||
if [[ "${1:-}" == "--force-failure" ]]; then
|
||||
@@ -65,6 +77,7 @@ fi
|
||||
if [[ "${1:-}" == "--cleanup-probe" ]]; then
|
||||
before_database_count="$(mysql --batch --skip-column-names -e "SELECT COUNT(*) FROM information_schema.SCHEMATA WHERE SCHEMA_NAME LIKE 'qipai_m01c_test_%'")"
|
||||
before_user_count="$(mysql --batch --skip-column-names -e "SELECT COUNT(*) FROM mysql.user WHERE User LIKE 'qmb_%' AND Host = '127.0.0.1'")"
|
||||
before_trust_function_creators="$(mysql --batch --skip-column-names -e 'SELECT @@GLOBAL.log_bin_trust_function_creators')"
|
||||
set +e
|
||||
bash "${BASH_SOURCE[0]}" --force-failure
|
||||
probe_status=$?
|
||||
@@ -75,7 +88,8 @@ if [[ "${1:-}" == "--cleanup-probe" ]]; then
|
||||
}
|
||||
after_database_count="$(mysql --batch --skip-column-names -e "SELECT COUNT(*) FROM information_schema.SCHEMATA WHERE SCHEMA_NAME LIKE 'qipai_m01c_test_%'")"
|
||||
after_user_count="$(mysql --batch --skip-column-names -e "SELECT COUNT(*) FROM mysql.user WHERE User LIKE 'qmb_%' AND Host = '127.0.0.1'")"
|
||||
if [[ "${before_database_count}" != "${after_database_count}" || "${before_user_count}" != "${after_user_count}" ]]; then
|
||||
after_trust_function_creators="$(mysql --batch --skip-column-names -e 'SELECT @@GLOBAL.log_bin_trust_function_creators')"
|
||||
if [[ "${before_database_count}" != "${after_database_count}" || "${before_user_count}" != "${after_user_count}" || "${before_trust_function_creators}" != "${after_trust_function_creators}" ]]; then
|
||||
echo "FAIL: temporary MySQL resources survived an interrupted test." >&2
|
||||
exit 1
|
||||
fi
|
||||
@@ -93,6 +107,6 @@ export QIPAI_MYSQL_USER="${username}"
|
||||
export QIPAI_MYSQL_PASSWORD="${password}"
|
||||
export QIPAI_MYSQL_CONNECTION_LIMIT=2
|
||||
|
||||
echo "INFO: MySQL ${mysql_version}; running M01-B through M09-C integration roundtrip in a temporary database."
|
||||
echo "INFO: MySQL ${mysql_version}; running M01-B through M09-D1 integration roundtrip in a temporary database."
|
||||
npm --prefix backend run test:mysql:migration
|
||||
echo "PASS: M01-B through M09-C live MySQL integration roundtrip completed."
|
||||
echo "PASS: M01-B through M09-D1 live MySQL integration roundtrip completed."
|
||||
|
||||
Reference in New Issue
Block a user