feat(M09-D1): 完成商品目录与库存流水底座

This commit is contained in:
Codex
2026-08-11 03:51:18 +08:00
parent 2de796143f
commit 93af128002
37 changed files with 14683 additions and 58 deletions
@@ -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.');
+11
View File
@@ -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"
)
+9 -5
View File
@@ -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}`?') {
+2
View File
@@ -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
+17 -3
View File
@@ -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."