From 8801881f9c9eb2622af836f191df7c613a858125 Mon Sep 17 00:00:00 2001 From: Codex Date: Tue, 16 Jun 2026 21:15:29 +0800 Subject: [PATCH] =?UTF-8?q?feat(M01-B):=20=E5=BB=BA=E7=AB=8B=E6=A0=B8?= =?UTF-8?q?=E5=BF=83=E8=BF=81=E7=A7=BB=E4=B8=8EMySQL=E8=BF=9E=E6=8E=A5?= =?UTF-8?q?=E6=B1=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/package.json | 2 +- backend/src/config.ts | 5 +- backend/src/db/mysql.ts | 23 +++ backend/tests/migration-contract.test.mjs | 64 ++++++ backend/tests/mysql-pool-contract.test.mjs | 29 +++ .../2026061601_m01b_core_schema.down.sql | 13 ++ .../2026061601_m01b_core_schema.up.sql | 184 ++++++++++++++++++ .../2026061601_m01b_core_schema.verify.sql | 37 ++++ .../seeds/2026061601_m01b_minimal_seed.sql | 22 +++ scripts/dev/windows/check-backend.ps1 | 7 + .../dev/windows/check-release-manifest.ps1 | 8 +- .../dev/windows/check-repo-completeness.ps1 | 7 + scripts/setup/deploy-business.sh | 10 +- 13 files changed, 404 insertions(+), 7 deletions(-) create mode 100644 backend/src/db/mysql.ts create mode 100644 backend/tests/migration-contract.test.mjs create mode 100644 backend/tests/mysql-pool-contract.test.mjs create mode 100644 database/migrations/2026061601_m01b_core_schema.down.sql create mode 100644 database/migrations/2026061601_m01b_core_schema.up.sql create mode 100644 database/migrations/2026061601_m01b_core_schema.verify.sql create mode 100644 database/seeds/2026061601_m01b_minimal_seed.sql diff --git a/backend/package.json b/backend/package.json index 7e88728..50d8f17 100644 --- a/backend/package.json +++ b/backend/package.json @@ -11,7 +11,7 @@ "dev": "tsx watch src/server.ts", "build": "tsc -p tsconfig.json", "start": "node dist/server.js", - "test": "node tests/backend-contract.test.mjs" + "test": "npm run build && node tests/backend-contract.test.mjs && node tests/migration-contract.test.mjs && node tests/mysql-pool-contract.test.mjs" }, "dependencies": { "@fastify/cors": "^11.2.0", diff --git a/backend/src/config.ts b/backend/src/config.ts index e43bcbf..b8ff47f 100644 --- a/backend/src/config.ts +++ b/backend/src/config.ts @@ -11,6 +11,7 @@ const configSchema = z.object({ QIPAI_MYSQL_DATABASE: z.string().min(1).default('qipai'), QIPAI_MYSQL_USER: z.string().min(1).default('qipai_app'), QIPAI_MYSQL_PASSWORD: z.string().default(''), + QIPAI_MYSQL_CONNECTION_LIMIT: z.coerce.number().int().min(1).max(50).default(10), QIPAI_MQTT_URL: z.string().url().default('mqtt://101.42.38.246:1883'), QIPAI_MQTT_USERNAME: z.string().default(''), QIPAI_MQTT_PASSWORD: z.string().default('') @@ -32,7 +33,9 @@ export function loadConfig(env: NodeJS.ProcessEnv = process.env) { port: parsed.QIPAI_MYSQL_PORT, database: parsed.QIPAI_MYSQL_DATABASE, user: parsed.QIPAI_MYSQL_USER, - passwordConfigured: parsed.QIPAI_MYSQL_PASSWORD.length > 0 + password: parsed.QIPAI_MYSQL_PASSWORD, + passwordConfigured: parsed.QIPAI_MYSQL_PASSWORD.length > 0, + connectionLimit: parsed.QIPAI_MYSQL_CONNECTION_LIMIT }, mqtt: { url: parsed.QIPAI_MQTT_URL, diff --git a/backend/src/db/mysql.ts b/backend/src/db/mysql.ts new file mode 100644 index 0000000..2d1558f --- /dev/null +++ b/backend/src/db/mysql.ts @@ -0,0 +1,23 @@ +import mysql, { type Pool, type PoolOptions } from 'mysql2/promise'; +import type { AppConfig } from '../config.js'; + +export type MySqlPool = Pool; + +export function createMySqlPool(config: AppConfig): MySqlPool { + return mysql.createPool(toPoolOptions(config)); +} + +export function toPoolOptions(config: AppConfig): PoolOptions { + return { + host: config.mysql.host, + port: config.mysql.port, + database: config.mysql.database, + user: config.mysql.user, + password: config.mysql.password, + waitForConnections: true, + connectionLimit: config.mysql.connectionLimit, + namedPlaceholders: true, + timezone: 'Z', + dateStrings: false + }; +} diff --git a/backend/tests/migration-contract.test.mjs b/backend/tests/migration-contract.test.mjs new file mode 100644 index 0000000..8e8e3d6 --- /dev/null +++ b/backend/tests/migration-contract.test.mjs @@ -0,0 +1,64 @@ +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const backendRoot = dirname(dirname(fileURLToPath(import.meta.url))); +const repoRoot = dirname(backendRoot); +const read = (path) => readFileSync(join(repoRoot, path), 'utf8'); + +const upSql = read('database/migrations/2026061601_m01b_core_schema.up.sql'); +const downSql = read('database/migrations/2026061601_m01b_core_schema.down.sql'); +const verifySql = read('database/migrations/2026061601_m01b_core_schema.verify.sql'); +const seedSql = read('database/seeds/2026061601_m01b_minimal_seed.sql'); + +const coreTables = [ + 'qipai_schema_migrations', + 'qipai_tenants', + 'qipai_stores', + 'qipai_rooms', + 'qipai_members', + 'qipai_orders', + 'qipai_payments', + 'qipai_devices', + 'qipai_audit_logs', + 'qipai_legacy_table_mappings' +]; + +for (const table of coreTables) { + assert.match(upSql, new RegExp(`CREATE TABLE IF NOT EXISTS ${table}\\b`)); + assert.match(downSql, new RegExp(`DROP TABLE IF EXISTS ${table}\\b`)); + assert.match(verifySql, new RegExp(`'${table}'`)); +} + +for (const table of [ + 'qipai_stores', + 'qipai_rooms', + 'qipai_members', + 'qipai_orders', + 'qipai_payments', + 'qipai_devices', + 'qipai_audit_logs' +]) { + const block = upSql.slice(upSql.indexOf(`CREATE TABLE IF NOT EXISTS ${table}`)); + assert.match(block, /\btenant_id BIGINT UNSIGNED NOT NULL\b/); +} + +for (const amountColumn of [ + 'base_price_cents', + 'balance_cents', + 'total_amount_cents', + 'paid_amount_cents', + 'amount_cents' +]) { + assert.match(upSql, new RegExp(`\\b${amountColumn}\\b`)); +} + +assert.doesNotMatch(upSql, /\bDECIMAL\b/i); +assert.match(upSql, /DATETIME\(3\)/); +assert.match(upSql, /legacy_store_id/); +assert.match(upSql, /legacy_order_id/); +assert.match(seedSql, /INSERT IGNORE INTO qipai_legacy_table_mappings/); +assert.match(seedSql, /member_order_info/); + +console.log('PASS: M01-B migration contract is present.'); diff --git a/backend/tests/mysql-pool-contract.test.mjs b/backend/tests/mysql-pool-contract.test.mjs new file mode 100644 index 0000000..ee8d5cf --- /dev/null +++ b/backend/tests/mysql-pool-contract.test.mjs @@ -0,0 +1,29 @@ +import assert from 'node:assert/strict'; +import { loadConfig } from '../dist/config.js'; +import { toPoolOptions } from '../dist/db/mysql.js'; + +const config = loadConfig({ + NODE_ENV: 'test', + QIPAI_MYSQL_HOST: 'db.internal', + QIPAI_MYSQL_PORT: '3307', + QIPAI_MYSQL_DATABASE: 'qipai_test', + QIPAI_MYSQL_USER: 'qipai_test_user', + QIPAI_MYSQL_PASSWORD: 'test-password', + QIPAI_MYSQL_CONNECTION_LIMIT: '7', + QIPAI_MQTT_URL: 'mqtt://127.0.0.1:1883' +}); + +const poolOptions = toPoolOptions(config); + +assert.equal(config.mysql.passwordConfigured, true); +assert.equal(poolOptions.host, 'db.internal'); +assert.equal(poolOptions.port, 3307); +assert.equal(poolOptions.database, 'qipai_test'); +assert.equal(poolOptions.user, 'qipai_test_user'); +assert.equal(poolOptions.password, 'test-password'); +assert.equal(poolOptions.connectionLimit, 7); +assert.equal(poolOptions.waitForConnections, true); +assert.equal(poolOptions.namedPlaceholders, true); +assert.equal(poolOptions.timezone, 'Z'); + +console.log('PASS: MySQL pool contract is present.'); diff --git a/database/migrations/2026061601_m01b_core_schema.down.sql b/database/migrations/2026061601_m01b_core_schema.down.sql new file mode 100644 index 0000000..be08ebd --- /dev/null +++ b/database/migrations/2026061601_m01b_core_schema.down.sql @@ -0,0 +1,13 @@ +-- Roll back M01-B core schema baseline. +-- This down migration is intended for empty development databases or pre-production rehearsals only. + +DROP TABLE IF EXISTS qipai_legacy_table_mappings; +DROP TABLE IF EXISTS qipai_audit_logs; +DROP TABLE IF EXISTS qipai_devices; +DROP TABLE IF EXISTS qipai_payments; +DROP TABLE IF EXISTS qipai_orders; +DROP TABLE IF EXISTS qipai_members; +DROP TABLE IF EXISTS qipai_rooms; +DROP TABLE IF EXISTS qipai_stores; +DROP TABLE IF EXISTS qipai_tenants; +DROP TABLE IF EXISTS qipai_schema_migrations; diff --git a/database/migrations/2026061601_m01b_core_schema.up.sql b/database/migrations/2026061601_m01b_core_schema.up.sql new file mode 100644 index 0000000..0dc8140 --- /dev/null +++ b/database/migrations/2026061601_m01b_core_schema.up.sql @@ -0,0 +1,184 @@ +-- M01-B core schema baseline. +-- Time columns are stored as UTC DATETIME(3). Amount columns use integer cents. + +CREATE TABLE IF NOT EXISTS qipai_schema_migrations ( + version VARCHAR(32) NOT NULL PRIMARY KEY, + name VARCHAR(128) NOT NULL, + applied_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS qipai_tenants ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + code VARCHAR(64) NOT NULL, + name VARCHAR(128) NOT NULL, + status VARCHAR(32) NOT NULL DEFAULT 'ACTIVE', + timezone VARCHAR(64) NOT NULL DEFAULT 'Asia/Shanghai', + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + deleted_at DATETIME(3) NULL, + UNIQUE KEY uq_qipai_tenants_code (code), + KEY idx_qipai_tenants_status (status) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS qipai_stores ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + tenant_id BIGINT UNSIGNED NOT NULL, + legacy_store_id BIGINT UNSIGNED NULL, + name VARCHAR(128) NOT NULL, + address VARCHAR(255) NOT NULL DEFAULT '', + contact_phone VARCHAR(32) NOT NULL DEFAULT '', + timezone VARCHAR(64) NOT NULL DEFAULT 'Asia/Shanghai', + business_status VARCHAR(32) NOT NULL DEFAULT 'OPEN', + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + deleted_at DATETIME(3) NULL, + CONSTRAINT fk_qipai_stores_tenant FOREIGN KEY (tenant_id) REFERENCES qipai_tenants(id), + UNIQUE KEY uq_qipai_stores_tenant_legacy (tenant_id, legacy_store_id), + KEY idx_qipai_stores_tenant_status (tenant_id, business_status), + KEY idx_qipai_stores_deleted_at (deleted_at) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS qipai_rooms ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + tenant_id BIGINT UNSIGNED NOT NULL, + store_id BIGINT UNSIGNED NOT NULL, + legacy_room_id BIGINT UNSIGNED NULL, + name VARCHAR(128) NOT NULL, + room_no VARCHAR(64) NOT NULL, + capacity INT UNSIGNED NOT NULL DEFAULT 4, + base_price_cents INT UNSIGNED NOT NULL DEFAULT 0, + status VARCHAR(32) NOT NULL DEFAULT 'AVAILABLE', + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + deleted_at DATETIME(3) NULL, + CONSTRAINT fk_qipai_rooms_tenant FOREIGN KEY (tenant_id) REFERENCES qipai_tenants(id), + CONSTRAINT fk_qipai_rooms_store FOREIGN KEY (store_id) REFERENCES qipai_stores(id), + UNIQUE KEY uq_qipai_rooms_store_no (tenant_id, store_id, room_no), + UNIQUE KEY uq_qipai_rooms_tenant_legacy (tenant_id, legacy_room_id), + KEY idx_qipai_rooms_tenant_status (tenant_id, status), + KEY idx_qipai_rooms_deleted_at (deleted_at) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS qipai_members ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + tenant_id BIGINT UNSIGNED NOT NULL, + legacy_member_id BIGINT UNSIGNED NULL, + openid VARCHAR(128) NULL, + unionid VARCHAR(128) NULL, + nickname VARCHAR(128) NOT NULL DEFAULT '', + phone VARCHAR(32) NOT NULL DEFAULT '', + balance_cents INT UNSIGNED NOT NULL DEFAULT 0, + status VARCHAR(32) NOT NULL DEFAULT 'ACTIVE', + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + deleted_at DATETIME(3) NULL, + CONSTRAINT fk_qipai_members_tenant FOREIGN KEY (tenant_id) REFERENCES qipai_tenants(id), + UNIQUE KEY uq_qipai_members_tenant_openid (tenant_id, openid), + UNIQUE KEY uq_qipai_members_tenant_legacy (tenant_id, legacy_member_id), + KEY idx_qipai_members_tenant_phone (tenant_id, phone), + KEY idx_qipai_members_deleted_at (deleted_at) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS qipai_orders ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + tenant_id BIGINT UNSIGNED NOT NULL, + store_id BIGINT UNSIGNED NOT NULL, + room_id BIGINT UNSIGNED NOT NULL, + member_id BIGINT UNSIGNED NULL, + legacy_order_id BIGINT UNSIGNED NULL, + order_no VARCHAR(64) NOT NULL, + status VARCHAR(32) NOT NULL DEFAULT 'DRAFT', + start_at DATETIME(3) NOT NULL, + end_at DATETIME(3) NOT NULL, + total_amount_cents INT UNSIGNED NOT NULL DEFAULT 0, + paid_amount_cents INT UNSIGNED NOT NULL DEFAULT 0, + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + deleted_at DATETIME(3) NULL, + CONSTRAINT fk_qipai_orders_tenant FOREIGN KEY (tenant_id) REFERENCES qipai_tenants(id), + CONSTRAINT fk_qipai_orders_store FOREIGN KEY (store_id) REFERENCES qipai_stores(id), + CONSTRAINT fk_qipai_orders_room FOREIGN KEY (room_id) REFERENCES qipai_rooms(id), + CONSTRAINT fk_qipai_orders_member FOREIGN KEY (member_id) REFERENCES qipai_members(id), + UNIQUE KEY uq_qipai_orders_tenant_order_no (tenant_id, order_no), + UNIQUE KEY uq_qipai_orders_tenant_legacy (tenant_id, legacy_order_id), + KEY idx_qipai_orders_room_time (tenant_id, room_id, start_at, end_at), + KEY idx_qipai_orders_tenant_status (tenant_id, status), + KEY idx_qipai_orders_deleted_at (deleted_at) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS qipai_payments ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + tenant_id BIGINT UNSIGNED NOT NULL, + order_id BIGINT UNSIGNED NOT NULL, + legacy_pay_order_id BIGINT UNSIGNED NULL, + payment_no VARCHAR(64) NOT NULL, + channel VARCHAR(32) NOT NULL DEFAULT 'WECHAT', + status VARCHAR(32) NOT NULL DEFAULT 'PENDING', + amount_cents INT UNSIGNED NOT NULL, + paid_at DATETIME(3) NULL, + raw_notify JSON NULL, + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + deleted_at DATETIME(3) NULL, + CONSTRAINT fk_qipai_payments_tenant FOREIGN KEY (tenant_id) REFERENCES qipai_tenants(id), + CONSTRAINT fk_qipai_payments_order FOREIGN KEY (order_id) REFERENCES qipai_orders(id), + UNIQUE KEY uq_qipai_payments_tenant_payment_no (tenant_id, payment_no), + UNIQUE KEY uq_qipai_payments_tenant_legacy (tenant_id, legacy_pay_order_id), + KEY idx_qipai_payments_tenant_status (tenant_id, status), + KEY idx_qipai_payments_deleted_at (deleted_at) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS qipai_devices ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + tenant_id BIGINT UNSIGNED NOT NULL, + store_id BIGINT UNSIGNED NOT NULL, + room_id BIGINT UNSIGNED NULL, + legacy_device_id BIGINT UNSIGNED NULL, + device_id VARCHAR(64) NOT NULL, + imei VARCHAR(64) NOT NULL DEFAULT '', + device_type VARCHAR(32) NOT NULL, + status VARCHAR(32) NOT NULL DEFAULT 'OFFLINE', + last_seen_at DATETIME(3) NULL, + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + deleted_at DATETIME(3) NULL, + CONSTRAINT fk_qipai_devices_tenant FOREIGN KEY (tenant_id) REFERENCES qipai_tenants(id), + CONSTRAINT fk_qipai_devices_store FOREIGN KEY (store_id) REFERENCES qipai_stores(id), + CONSTRAINT fk_qipai_devices_room FOREIGN KEY (room_id) REFERENCES qipai_rooms(id), + UNIQUE KEY uq_qipai_devices_tenant_device (tenant_id, device_id), + UNIQUE KEY uq_qipai_devices_tenant_legacy (tenant_id, legacy_device_id), + KEY idx_qipai_devices_tenant_type_status (tenant_id, device_type, status), + KEY idx_qipai_devices_deleted_at (deleted_at) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS qipai_audit_logs ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + tenant_id BIGINT UNSIGNED NOT NULL, + actor_type VARCHAR(32) NOT NULL, + actor_id BIGINT UNSIGNED NULL, + action VARCHAR(128) NOT NULL, + resource_type VARCHAR(64) NOT NULL, + resource_id BIGINT UNSIGNED NULL, + trace_id VARCHAR(128) NOT NULL, + ip VARCHAR(64) NOT NULL DEFAULT '', + user_agent VARCHAR(255) NOT NULL DEFAULT '', + metadata JSON NULL, + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + CONSTRAINT fk_qipai_audit_logs_tenant FOREIGN KEY (tenant_id) REFERENCES qipai_tenants(id), + KEY idx_qipai_audit_logs_tenant_time (tenant_id, created_at), + KEY idx_qipai_audit_logs_trace_id (trace_id), + KEY idx_qipai_audit_logs_resource (tenant_id, resource_type, resource_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS qipai_legacy_table_mappings ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + legacy_table VARCHAR(128) NOT NULL, + new_table VARCHAR(128) NOT NULL, + strategy VARCHAR(32) NOT NULL, + note VARCHAR(512) NOT NULL DEFAULT '', + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + UNIQUE KEY uq_qipai_legacy_table_mappings_pair (legacy_table, new_table) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +INSERT IGNORE INTO qipai_schema_migrations (version, name) +VALUES ('2026061601', 'm01b_core_schema'); diff --git a/database/migrations/2026061601_m01b_core_schema.verify.sql b/database/migrations/2026061601_m01b_core_schema.verify.sql new file mode 100644 index 0000000..773e0e2 --- /dev/null +++ b/database/migrations/2026061601_m01b_core_schema.verify.sql @@ -0,0 +1,37 @@ +-- Verify that the M01-B core schema baseline exists. + +SELECT table_name +FROM information_schema.tables +WHERE table_schema = DATABASE() + AND table_name IN ( + 'qipai_schema_migrations', + 'qipai_tenants', + 'qipai_stores', + 'qipai_rooms', + 'qipai_members', + 'qipai_orders', + 'qipai_payments', + 'qipai_devices', + 'qipai_audit_logs', + 'qipai_legacy_table_mappings' + ) +ORDER BY table_name; + +SELECT table_name, column_name +FROM information_schema.columns +WHERE table_schema = DATABASE() + AND table_name IN ( + 'qipai_stores', + 'qipai_rooms', + 'qipai_members', + 'qipai_orders', + 'qipai_payments', + 'qipai_devices', + 'qipai_audit_logs' + ) + AND column_name IN ('tenant_id', 'created_at', 'updated_at', 'deleted_at') +ORDER BY table_name, column_name; + +SELECT version, name +FROM qipai_schema_migrations +WHERE version = '2026061601'; diff --git a/database/seeds/2026061601_m01b_minimal_seed.sql b/database/seeds/2026061601_m01b_minimal_seed.sql new file mode 100644 index 0000000..c8356de --- /dev/null +++ b/database/seeds/2026061601_m01b_minimal_seed.sql @@ -0,0 +1,22 @@ +-- Minimal non-production seed data for M01-B local verification. +-- Do not insert real user data or production credentials here. + +INSERT IGNORE INTO qipai_tenants (id, code, name, status, timezone) +VALUES (1, 'demo', '演示租户', 'ACTIVE', 'Asia/Shanghai'); + +INSERT IGNORE INTO qipai_stores (id, tenant_id, legacy_store_id, name, address, contact_phone, timezone, business_status) +VALUES (1, 1, NULL, '演示门店', '本地开发地址', '', 'Asia/Shanghai', 'OPEN'); + +INSERT IGNORE INTO qipai_rooms (id, tenant_id, store_id, legacy_room_id, name, room_no, capacity, base_price_cents, status) +VALUES (1, 1, 1, NULL, '演示包间', 'A001', 4, 3000, 'AVAILABLE'); + +INSERT IGNORE INTO qipai_legacy_table_mappings (legacy_table, new_table, strategy, note) +VALUES + ('system_tenant', 'qipai_tenants', 'REMODEL', '保留多租户概念,重建首期租户表。'), + ('member_store_info', 'qipai_stores', 'REMODEL', '门店字段按新系统重新命名,保留 legacy_store_id。'), + ('member_room_info', 'qipai_rooms', 'REMODEL', '房间价格统一转为整数分。'), + ('member_user', 'qipai_members', 'REMODEL', '会员余额统一转为整数分。'), + ('member_order_info', 'qipai_orders', 'REMODEL', '订单时间统一 DATETIME(3),内部按 UTC 保存。'), + ('member_pay_order', 'qipai_payments', 'REMODEL', '支付金额统一整数分,原始回调存 JSON。'), + ('member_device_info', 'qipai_devices', 'REMODEL', '设备 ID 与 IMEI 分离,后续 M06 接入 MQTT。'), + ('system_operate_log', 'qipai_audit_logs', 'REMODEL', '统一 traceId 与资源审计字段。'); diff --git a/scripts/dev/windows/check-backend.ps1 b/scripts/dev/windows/check-backend.ps1 index 15649c0..b520c1e 100644 --- a/scripts/dev/windows/check-backend.ps1 +++ b/scripts/dev/windows/check-backend.ps1 @@ -7,9 +7,16 @@ $requiredFiles = @( "backend/.env.example", "backend/src/app.ts", "backend/src/config.ts", + "backend/src/db/mysql.ts", "backend/src/routes/health.ts", "backend/src/server.ts", "backend/tests/backend-contract.test.mjs", + "backend/tests/migration-contract.test.mjs", + "backend/tests/mysql-pool-contract.test.mjs", + "database/migrations/2026061601_m01b_core_schema.up.sql", + "database/migrations/2026061601_m01b_core_schema.down.sql", + "database/migrations/2026061601_m01b_core_schema.verify.sql", + "database/seeds/2026061601_m01b_minimal_seed.sql", "deploy/pm2/ecosystem.config.cjs" ) diff --git a/scripts/dev/windows/check-release-manifest.ps1 b/scripts/dev/windows/check-release-manifest.ps1 index 856c995..b7cbb18 100644 --- a/scripts/dev/windows/check-release-manifest.ps1 +++ b/scripts/dev/windows/check-release-manifest.ps1 @@ -4,6 +4,8 @@ $head = (& git rev-parse HEAD).Trim() $shortHead = (& git rev-parse --short HEAD).Trim() $backendBuild = if (Test-Path "backend/package.json") { "PROJECT_PRESENT_BUILD_NOT_RUN" } else { "SKIPPED_NO_PROJECT" } $adminBuild = if (Test-Path "admin/package.json") { "PROJECT_PRESENT_BUILD_NOT_RUN" } else { "SKIPPED_NO_PROJECT" } +$migrationFiles = @(Get-ChildItem -Path "database/migrations" -Filter "*.sql" -File -ErrorAction SilentlyContinue) +$databaseMigration = if ($migrationFiles.Count -gt 0) { "PROJECT_PRESENT_MIGRATION_NOT_RUN" } else { "SKIPPED_NO_MIGRATIONS" } $rawText = @" { @@ -14,7 +16,7 @@ $rawText = @" "backendBuild": "$backendBuild", "adminBuild": "$adminBuild", "miniappMirror": "RECORDED_SOURCE_COMMIT_ONLY", - "databaseMigration": "SKIPPED_NO_MIGRATIONS", + "databaseMigration": "$databaseMigration", "deployed": false } "@ @@ -55,8 +57,8 @@ if ((-not (Test-Path "admin/package.json")) -and $manifest.adminBuild -ne "SKIPP throw "Dry-run manifest should record missing admin project" } -if ($manifest.databaseMigration -ne "SKIPPED_NO_MIGRATIONS") { - throw "Dry-run manifest should record skipped migrations until migrations exist" +if ($manifest.databaseMigration -ne $databaseMigration) { + throw "Dry-run manifest databaseMigration mismatch: $($manifest.databaseMigration)" } Write-Host "PASS: release manifest dry-run is valid for current HEAD." diff --git a/scripts/dev/windows/check-repo-completeness.ps1 b/scripts/dev/windows/check-repo-completeness.ps1 index c382247..5f8dbbb 100644 --- a/scripts/dev/windows/check-repo-completeness.ps1 +++ b/scripts/dev/windows/check-repo-completeness.ps1 @@ -22,6 +22,13 @@ $requiredPaths = @( "docs/repository-completeness.md", "scripts/dev/windows/check-workspace.ps1", "backend/package-lock.json", + "backend/src/db/mysql.ts", + "backend/tests/migration-contract.test.mjs", + "backend/tests/mysql-pool-contract.test.mjs", + "database/migrations/2026061601_m01b_core_schema.up.sql", + "database/migrations/2026061601_m01b_core_schema.down.sql", + "database/migrations/2026061601_m01b_core_schema.verify.sql", + "database/seeds/2026061601_m01b_minimal_seed.sql", "scripts/dev/windows/check-backend.ps1", "scripts/dev/windows/check-reference.ps1", "scripts/dev/windows/check-repo-completeness.ps1", diff --git a/scripts/setup/deploy-business.sh b/scripts/setup/deploy-business.sh index d890e7f..094abcb 100644 --- a/scripts/setup/deploy-business.sh +++ b/scripts/setup/deploy-business.sh @@ -11,7 +11,7 @@ qipai_release_manifest_json() { local repo_dir="$1" local release_id="$2" local deployed="$3" - local backend_build admin_build + local backend_build admin_build database_migration if [ -f "${repo_dir}/backend/package.json" ]; then backend_build="PROJECT_PRESENT_BUILD_NOT_RUN" @@ -25,6 +25,12 @@ qipai_release_manifest_json() { admin_build="SKIPPED_NO_PROJECT" fi + if find "${repo_dir}/database/migrations" -maxdepth 1 -type f -name '*.sql' | grep -q .; then + database_migration="PROJECT_PRESENT_MIGRATION_NOT_RUN" + else + database_migration="SKIPPED_NO_MIGRATIONS" + fi + cat <