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,36 @@
DROP TRIGGER IF EXISTS qipai_product_inventory_ledger_no_delete;
DROP TRIGGER IF EXISTS qipai_product_inventory_ledger_no_update;
DELETE rp FROM qipai_role_permissions rp
INNER JOIN qipai_permissions p ON p.id = rp.permission_id
WHERE p.code IN (
'product.catalog.read', 'product.catalog.write',
'inventory.read', 'inventory.adjust'
);
DELETE FROM qipai_permissions
WHERE code IN (
'product.catalog.read', 'product.catalog.write',
'inventory.read', 'inventory.adjust'
);
DROP TABLE IF EXISTS qipai_product_inventory_ledger;
DROP TABLE IF EXISTS qipai_product_inventory_requests;
DROP TABLE IF EXISTS qipai_product_inventory;
DROP TABLE IF EXISTS qipai_product_store_hours;
DROP TABLE IF EXISTS qipai_product_store_settings;
DROP TABLE IF EXISTS qipai_product_store_listings;
DROP TABLE IF EXISTS qipai_product_skus;
DROP TABLE IF EXISTS qipai_products;
DROP TABLE IF EXISTS qipai_product_categories;
ALTER TABLE qipai_stores
DROP INDEX uq_qipai_stores_tenant_id;
ALTER TABLE qipai_users
DROP INDEX uq_qipai_users_tenant_id;
ALTER TABLE qipai_schema_migrations
DROP COLUMN checksum;
DELETE FROM qipai_schema_migrations WHERE version = '2026081107';
@@ -0,0 +1,441 @@
ALTER TABLE qipai_stores
ADD UNIQUE KEY uq_qipai_stores_tenant_id (tenant_id, id);
ALTER TABLE qipai_users
ADD UNIQUE KEY uq_qipai_users_tenant_id (tenant_id, id);
ALTER TABLE qipai_schema_migrations
ADD COLUMN checksum CHAR(64) CHARACTER SET ascii COLLATE ascii_bin
NOT NULL DEFAULT '' AFTER name;
CREATE TABLE IF NOT EXISTS qipai_product_categories (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
tenant_id BIGINT UNSIGNED NOT NULL,
store_id BIGINT UNSIGNED NOT NULL,
parent_id BIGINT UNSIGNED NULL,
category_code VARCHAR(64) NOT NULL,
name VARCHAR(128) NOT NULL,
description VARCHAR(1024) NOT NULL DEFAULT '',
image_url VARCHAR(512) NOT NULL DEFAULT '',
status VARCHAR(16) NOT NULL DEFAULT 'ACTIVE',
sort_order INT NOT NULL DEFAULT 0,
version INT UNSIGNED NOT NULL DEFAULT 1,
created_by BIGINT UNSIGNED NOT NULL,
updated_by BIGINT UNSIGNED NOT 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,
active_code_key VARCHAR(64)
GENERATED ALWAYS AS (CASE WHEN deleted_at IS NULL THEN category_code ELSE NULL END) STORED,
CONSTRAINT fk_qipai_product_category_tenant
FOREIGN KEY (tenant_id) REFERENCES qipai_tenants(id),
CONSTRAINT fk_qipai_product_category_store
FOREIGN KEY (tenant_id, store_id) REFERENCES qipai_stores(tenant_id, id),
CONSTRAINT fk_qipai_product_category_parent
FOREIGN KEY (tenant_id, store_id, parent_id)
REFERENCES qipai_product_categories(tenant_id, store_id, id),
CONSTRAINT fk_qipai_product_category_created_by
FOREIGN KEY (tenant_id, created_by) REFERENCES qipai_users(tenant_id, id),
CONSTRAINT fk_qipai_product_category_updated_by
FOREIGN KEY (tenant_id, updated_by) REFERENCES qipai_users(tenant_id, id),
UNIQUE KEY uq_qipai_product_category_scope_id (tenant_id, store_id, id),
UNIQUE KEY uq_qipai_product_category_active_code
(tenant_id, store_id, active_code_key),
KEY idx_qipai_product_category_parent
(tenant_id, store_id, parent_id, status, sort_order),
CONSTRAINT chk_qipai_product_category_status
CHECK (status IN ('ACTIVE', 'INACTIVE')),
CONSTRAINT chk_qipai_product_category_version CHECK (version >= 1)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS qipai_products (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
tenant_id BIGINT UNSIGNED NOT NULL,
product_code VARCHAR(64) NOT NULL,
name VARCHAR(128) NOT NULL,
unit_name VARCHAR(32) NOT NULL,
description TEXT NULL,
cover_url VARCHAR(512) NOT NULL DEFAULT '',
images_json JSON NULL,
delivery_enabled TINYINT(1) NOT NULL DEFAULT 1,
storage_enabled TINYINT(1) NOT NULL DEFAULT 0,
status VARCHAR(16) NOT NULL DEFAULT 'DRAFT',
sort_order INT NOT NULL DEFAULT 0,
version INT UNSIGNED NOT NULL DEFAULT 1,
created_by BIGINT UNSIGNED NOT NULL,
updated_by BIGINT UNSIGNED NOT 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,
active_code_key VARCHAR(64)
GENERATED ALWAYS AS (CASE WHEN deleted_at IS NULL THEN product_code ELSE NULL END) STORED,
CONSTRAINT fk_qipai_product_tenant
FOREIGN KEY (tenant_id) REFERENCES qipai_tenants(id),
CONSTRAINT fk_qipai_product_created_by
FOREIGN KEY (tenant_id, created_by) REFERENCES qipai_users(tenant_id, id),
CONSTRAINT fk_qipai_product_updated_by
FOREIGN KEY (tenant_id, updated_by) REFERENCES qipai_users(tenant_id, id),
UNIQUE KEY uq_qipai_product_tenant_id (tenant_id, id),
UNIQUE KEY uq_qipai_product_active_code (tenant_id, active_code_key),
KEY idx_qipai_product_catalog (tenant_id, status, sort_order),
CONSTRAINT chk_qipai_product_status
CHECK (status IN ('DRAFT', 'ACTIVE', 'INACTIVE')),
CONSTRAINT chk_qipai_product_capabilities CHECK (
delivery_enabled IN (0, 1) AND storage_enabled IN (0, 1)
),
CONSTRAINT chk_qipai_product_version CHECK (version >= 1)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS qipai_product_skus (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
tenant_id BIGINT UNSIGNED NOT NULL,
product_id BIGINT UNSIGNED NOT NULL,
sku_code VARCHAR(64) NOT NULL,
name VARCHAR(128) NOT NULL,
attributes_json JSON NULL,
barcode VARCHAR(64) NOT NULL DEFAULT '',
image_url VARCHAR(512) NOT NULL DEFAULT '',
sale_price_cents INT UNSIGNED NOT NULL,
market_price_cents INT UNSIGNED NOT NULL DEFAULT 0,
cost_price_cents INT UNSIGNED NOT NULL DEFAULT 0,
default_inventory_policy VARCHAR(16) NOT NULL DEFAULT 'TRACKED',
status VARCHAR(16) NOT NULL DEFAULT 'ACTIVE',
version INT UNSIGNED NOT NULL DEFAULT 1,
created_by BIGINT UNSIGNED NOT NULL,
updated_by BIGINT UNSIGNED NOT 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,
active_code_key VARCHAR(64)
GENERATED ALWAYS AS (CASE WHEN deleted_at IS NULL THEN sku_code ELSE NULL END) STORED,
active_barcode_key VARCHAR(64)
GENERATED ALWAYS AS (
CASE WHEN deleted_at IS NULL AND barcode <> '' THEN barcode ELSE NULL END
) STORED,
CONSTRAINT fk_qipai_product_sku_tenant
FOREIGN KEY (tenant_id) REFERENCES qipai_tenants(id),
CONSTRAINT fk_qipai_product_sku_product
FOREIGN KEY (tenant_id, product_id) REFERENCES qipai_products(tenant_id, id),
CONSTRAINT fk_qipai_product_sku_created_by
FOREIGN KEY (tenant_id, created_by) REFERENCES qipai_users(tenant_id, id),
CONSTRAINT fk_qipai_product_sku_updated_by
FOREIGN KEY (tenant_id, updated_by) REFERENCES qipai_users(tenant_id, id),
UNIQUE KEY uq_qipai_product_sku_tenant_id (tenant_id, id),
UNIQUE KEY uq_qipai_product_sku_active_code (tenant_id, active_code_key),
UNIQUE KEY uq_qipai_product_sku_active_barcode (tenant_id, active_barcode_key),
KEY idx_qipai_product_sku_product (tenant_id, product_id, status),
CONSTRAINT chk_qipai_product_sku_status CHECK (status IN ('ACTIVE', 'INACTIVE')),
CONSTRAINT chk_qipai_product_sku_inventory_policy CHECK (
default_inventory_policy IN ('TRACKED', 'UNLIMITED')
),
CONSTRAINT chk_qipai_product_sku_version CHECK (version >= 1),
CONSTRAINT chk_qipai_product_sku_amounts CHECK (
sale_price_cents <= 100000000
AND market_price_cents <= 100000000
AND cost_price_cents <= 100000000
)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS qipai_product_store_listings (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
tenant_id BIGINT UNSIGNED NOT NULL,
store_id BIGINT UNSIGNED NOT NULL,
product_id BIGINT UNSIGNED NOT NULL,
category_id BIGINT UNSIGNED NOT NULL,
status VARCHAR(16) NOT NULL DEFAULT 'INACTIVE',
fulfillment_mode VARCHAR(16) NOT NULL DEFAULT 'DELIVERY',
sales_start_at DATETIME(3) NULL,
sales_end_at DATETIME(3) NULL,
sort_order INT NOT NULL DEFAULT 0,
version INT UNSIGNED NOT NULL DEFAULT 1,
created_by BIGINT UNSIGNED NOT NULL,
updated_by BIGINT UNSIGNED NOT 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,
active_product_id BIGINT UNSIGNED
GENERATED ALWAYS AS (CASE WHEN deleted_at IS NULL THEN product_id ELSE NULL END) STORED,
CONSTRAINT fk_qipai_product_listing_tenant
FOREIGN KEY (tenant_id) REFERENCES qipai_tenants(id),
CONSTRAINT fk_qipai_product_listing_store
FOREIGN KEY (tenant_id, store_id) REFERENCES qipai_stores(tenant_id, id),
CONSTRAINT fk_qipai_product_listing_product
FOREIGN KEY (tenant_id, product_id) REFERENCES qipai_products(tenant_id, id),
CONSTRAINT fk_qipai_product_listing_category
FOREIGN KEY (tenant_id, store_id, category_id)
REFERENCES qipai_product_categories(tenant_id, store_id, id),
CONSTRAINT fk_qipai_product_listing_created_by
FOREIGN KEY (tenant_id, created_by) REFERENCES qipai_users(tenant_id, id),
CONSTRAINT fk_qipai_product_listing_updated_by
FOREIGN KEY (tenant_id, updated_by) REFERENCES qipai_users(tenant_id, id),
UNIQUE KEY uq_qipai_product_store_listing_active
(tenant_id, store_id, active_product_id),
KEY idx_qipai_product_store_listing_state
(tenant_id, store_id, category_id, status, sort_order),
CONSTRAINT chk_qipai_product_listing_status CHECK (status IN ('ACTIVE', 'INACTIVE')),
CONSTRAINT chk_qipai_product_listing_fulfillment
CHECK (fulfillment_mode IN ('DELIVERY', 'SELF_SERVICE', 'BOTH')),
CONSTRAINT chk_qipai_product_listing_window CHECK (
sales_start_at IS NULL OR sales_end_at IS NULL OR sales_end_at > sales_start_at
),
CONSTRAINT chk_qipai_product_listing_version CHECK (version >= 1)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS qipai_product_store_settings (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
tenant_id BIGINT UNSIGNED NOT NULL,
store_id BIGINT UNSIGNED NOT NULL,
sales_status VARCHAR(16) NOT NULL DEFAULT 'OPEN',
manual_paused_at DATETIME(3) NULL,
manual_paused_until DATETIME(3) NULL,
manual_pause_reason VARCHAR(512) NOT NULL DEFAULT '',
version INT UNSIGNED NOT NULL DEFAULT 1,
created_by BIGINT UNSIGNED NOT NULL,
updated_by BIGINT UNSIGNED NOT 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),
CONSTRAINT fk_qipai_product_store_setting_tenant
FOREIGN KEY (tenant_id) REFERENCES qipai_tenants(id),
CONSTRAINT fk_qipai_product_store_setting_store
FOREIGN KEY (tenant_id, store_id) REFERENCES qipai_stores(tenant_id, id),
CONSTRAINT fk_qipai_product_store_setting_created_by
FOREIGN KEY (tenant_id, created_by) REFERENCES qipai_users(tenant_id, id),
CONSTRAINT fk_qipai_product_store_setting_updated_by
FOREIGN KEY (tenant_id, updated_by) REFERENCES qipai_users(tenant_id, id),
UNIQUE KEY uq_qipai_product_store_setting (tenant_id, store_id),
UNIQUE KEY uq_qipai_product_store_setting_scope_id (tenant_id, store_id, id),
CONSTRAINT chk_qipai_product_store_setting_status
CHECK (sales_status IN ('OPEN', 'CLOSED')),
CONSTRAINT chk_qipai_product_store_setting_pause CHECK (
(manual_paused_at IS NULL
AND manual_paused_until IS NULL
AND manual_pause_reason = '')
OR (manual_paused_at IS NOT NULL
AND (manual_paused_until IS NULL OR manual_paused_until > manual_paused_at)
AND manual_pause_reason <> '')
),
CONSTRAINT chk_qipai_product_store_setting_version CHECK (version >= 1)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS qipai_product_store_hours (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
tenant_id BIGINT UNSIGNED NOT NULL,
settings_id BIGINT UNSIGNED NOT NULL,
store_id BIGINT UNSIGNED NOT NULL,
weekday TINYINT UNSIGNED NOT NULL,
slot_no TINYINT UNSIGNED NOT NULL,
open_minute SMALLINT UNSIGNED NOT NULL,
close_minute SMALLINT UNSIGNED NOT NULL,
crosses_midnight TINYINT(1) NOT NULL DEFAULT 0,
version INT UNSIGNED NOT NULL DEFAULT 1,
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,
active_slot_no TINYINT UNSIGNED
GENERATED ALWAYS AS (CASE WHEN deleted_at IS NULL THEN slot_no ELSE NULL END) STORED,
CONSTRAINT fk_qipai_product_store_hour_tenant
FOREIGN KEY (tenant_id) REFERENCES qipai_tenants(id),
CONSTRAINT fk_qipai_product_store_hour_setting
FOREIGN KEY (tenant_id, store_id, settings_id)
REFERENCES qipai_product_store_settings(tenant_id, store_id, id),
CONSTRAINT fk_qipai_product_store_hour_store
FOREIGN KEY (tenant_id, store_id) REFERENCES qipai_stores(tenant_id, id),
UNIQUE KEY uq_qipai_product_store_hour_active
(tenant_id, store_id, weekday, active_slot_no),
KEY idx_qipai_product_store_hour_window
(tenant_id, store_id, weekday, open_minute, close_minute),
CONSTRAINT chk_qipai_product_store_hour_weekday CHECK (weekday BETWEEN 1 AND 7),
CONSTRAINT chk_qipai_product_store_hour_slot CHECK (slot_no BETWEEN 1 AND 8),
CONSTRAINT chk_qipai_product_store_hour_window CHECK (
open_minute <= 1439
AND close_minute <= 1439
AND crosses_midnight IN (0, 1)
AND ((crosses_midnight = 0 AND close_minute > open_minute)
OR (crosses_midnight = 1 AND close_minute <= open_minute))
),
CONSTRAINT chk_qipai_product_store_hour_version CHECK (version >= 1)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS qipai_product_inventory (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
tenant_id BIGINT UNSIGNED NOT NULL,
store_id BIGINT UNSIGNED NOT NULL,
sku_id BIGINT UNSIGNED NOT NULL,
policy_type VARCHAR(16) NOT NULL DEFAULT 'TRACKED',
available_quantity BIGINT UNSIGNED NOT NULL DEFAULT 0,
locked_quantity BIGINT UNSIGNED NOT NULL DEFAULT 0,
loss_quantity BIGINT UNSIGNED NOT NULL DEFAULT 0,
low_stock_threshold BIGINT UNSIGNED NOT NULL DEFAULT 0,
version INT UNSIGNED NOT NULL DEFAULT 1,
updated_by BIGINT UNSIGNED 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),
CONSTRAINT fk_qipai_product_inventory_tenant
FOREIGN KEY (tenant_id) REFERENCES qipai_tenants(id),
CONSTRAINT fk_qipai_product_inventory_store
FOREIGN KEY (tenant_id, store_id) REFERENCES qipai_stores(tenant_id, id),
CONSTRAINT fk_qipai_product_inventory_sku
FOREIGN KEY (tenant_id, sku_id) REFERENCES qipai_product_skus(tenant_id, id),
CONSTRAINT fk_qipai_product_inventory_updated_by
FOREIGN KEY (tenant_id, updated_by) REFERENCES qipai_users(tenant_id, id),
UNIQUE KEY uq_qipai_product_inventory_scope (tenant_id, store_id, sku_id),
UNIQUE KEY uq_qipai_product_inventory_scope_id
(tenant_id, store_id, sku_id, id),
KEY idx_qipai_product_inventory_low_stock
(tenant_id, store_id, policy_type, available_quantity, low_stock_threshold),
CONSTRAINT chk_qipai_product_inventory_policy CHECK (
policy_type IN ('TRACKED', 'UNLIMITED')
AND (policy_type = 'TRACKED'
OR (available_quantity = 0
AND locked_quantity = 0
AND loss_quantity = 0
AND low_stock_threshold = 0))
),
CONSTRAINT chk_qipai_product_inventory_quantity CHECK (
available_quantity <= 1000000000
AND locked_quantity <= 1000000000
AND loss_quantity <= 1000000000
AND low_stock_threshold <= 1000000000
),
CONSTRAINT chk_qipai_product_inventory_version CHECK (version >= 1)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS qipai_product_inventory_requests (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
tenant_id BIGINT UNSIGNED NOT NULL,
store_id BIGINT UNSIGNED NOT NULL,
request_id VARCHAR(64) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
operation VARCHAR(32) NOT NULL,
business_type VARCHAR(64) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
business_id VARCHAR(128) COLLATE utf8mb4_bin NOT NULL,
fingerprint CHAR(64) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
item_count SMALLINT UNSIGNED NOT NULL,
operator_id BIGINT UNSIGNED NULL,
trace_id VARCHAR(128) NOT NULL,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
CONSTRAINT fk_qipai_product_inventory_request_tenant
FOREIGN KEY (tenant_id) REFERENCES qipai_tenants(id),
CONSTRAINT fk_qipai_product_inventory_request_store
FOREIGN KEY (tenant_id, store_id) REFERENCES qipai_stores(tenant_id, id),
CONSTRAINT fk_qipai_product_inventory_request_operator
FOREIGN KEY (tenant_id, operator_id) REFERENCES qipai_users(tenant_id, id),
UNIQUE KEY uq_qipai_product_inventory_request
(tenant_id, request_id),
KEY idx_qipai_product_inventory_request_business
(tenant_id, business_type, business_id, created_at, id),
CONSTRAINT chk_qipai_product_inventory_request_operation CHECK (
operation IN (
'CONFIGURE', 'INBOUND', 'ADJUST', 'LOCK', 'RELEASE',
'DEDUCT', 'STOCKTAKE', 'LOSS', 'RETURN'
)
),
CONSTRAINT chk_qipai_product_inventory_request_items CHECK (
item_count BETWEEN 1 AND 100
)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS qipai_product_inventory_ledger (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
tenant_id BIGINT UNSIGNED NOT NULL,
inventory_id BIGINT UNSIGNED NOT NULL,
store_id BIGINT UNSIGNED NOT NULL,
sku_id BIGINT UNSIGNED NOT NULL,
request_id VARCHAR(64) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
business_type VARCHAR(64) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
business_id VARCHAR(128) COLLATE utf8mb4_bin NOT NULL,
operation VARCHAR(32) NOT NULL,
available_delta BIGINT NOT NULL DEFAULT 0,
locked_delta BIGINT NOT NULL DEFAULT 0,
loss_delta BIGINT NOT NULL DEFAULT 0,
available_after BIGINT UNSIGNED NOT NULL,
locked_after BIGINT UNSIGNED NOT NULL,
loss_after BIGINT UNSIGNED NOT NULL,
version_after INT UNSIGNED NOT NULL,
operator_id BIGINT UNSIGNED NULL,
trace_id VARCHAR(128) NOT NULL,
reason VARCHAR(512) NOT NULL DEFAULT '',
metadata JSON NULL,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
CONSTRAINT fk_qipai_product_inventory_ledger_tenant
FOREIGN KEY (tenant_id) REFERENCES qipai_tenants(id),
CONSTRAINT fk_qipai_product_inventory_ledger_inventory
FOREIGN KEY (tenant_id, store_id, sku_id, inventory_id)
REFERENCES qipai_product_inventory(tenant_id, store_id, sku_id, id),
CONSTRAINT fk_qipai_product_inventory_ledger_store
FOREIGN KEY (tenant_id, store_id) REFERENCES qipai_stores(tenant_id, id),
CONSTRAINT fk_qipai_product_inventory_ledger_sku
FOREIGN KEY (tenant_id, sku_id) REFERENCES qipai_product_skus(tenant_id, id),
CONSTRAINT fk_qipai_product_inventory_ledger_operator
FOREIGN KEY (tenant_id, operator_id) REFERENCES qipai_users(tenant_id, id),
CONSTRAINT fk_qipai_product_inventory_ledger_request
FOREIGN KEY (tenant_id, request_id)
REFERENCES qipai_product_inventory_requests(tenant_id, request_id),
UNIQUE KEY uq_qipai_product_inventory_ledger_request
(tenant_id, inventory_id, request_id),
UNIQUE KEY uq_qipai_product_inventory_ledger_version
(tenant_id, inventory_id, version_after),
KEY idx_qipai_product_inventory_ledger_history
(tenant_id, store_id, sku_id, created_at, id),
KEY idx_qipai_product_inventory_ledger_business
(tenant_id, business_type, business_id),
CONSTRAINT chk_qipai_product_inventory_ledger_operation CHECK (
operation IN (
'CONFIGURE', 'INBOUND', 'ADJUST', 'LOCK', 'RELEASE',
'DEDUCT', 'STOCKTAKE', 'LOSS', 'RETURN'
)
),
CONSTRAINT chk_qipai_product_inventory_ledger_delta CHECK (
available_delta BETWEEN -1000000000 AND 1000000000
AND locked_delta BETWEEN -1000000000 AND 1000000000
AND loss_delta BETWEEN -1000000000 AND 1000000000
),
CONSTRAINT chk_qipai_product_inventory_ledger_after CHECK (
available_after <= 1000000000
AND locked_after <= 1000000000
AND loss_after <= 1000000000
AND version_after >= 1
)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
DROP TRIGGER IF EXISTS qipai_product_inventory_ledger_no_update;
CREATE TRIGGER qipai_product_inventory_ledger_no_update
BEFORE UPDATE ON qipai_product_inventory_ledger
FOR EACH ROW SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = 'PRODUCT_INVENTORY_LEDGER_IMMUTABLE';
DROP TRIGGER IF EXISTS qipai_product_inventory_ledger_no_delete;
CREATE TRIGGER qipai_product_inventory_ledger_no_delete
BEFORE DELETE ON qipai_product_inventory_ledger
FOR EACH ROW SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = 'PRODUCT_INVENTORY_LEDGER_IMMUTABLE';
INSERT IGNORE INTO qipai_permissions (code, name, category) VALUES
('product.catalog.read', '查看商品目录', 'product'),
('product.catalog.write', '管理商品目录', 'product'),
('inventory.read', '查看商品库存', 'inventory'),
('inventory.adjust', '调整商品库存', 'inventory');
INSERT IGNORE INTO qipai_role_permissions (tenant_id, role_id, permission_id)
SELECT r.tenant_id, r.id, p.id
FROM qipai_roles r
INNER JOIN qipai_permissions p
ON (r.code = 'STAFF'
AND p.code IN ('product.catalog.read', 'inventory.read'))
OR (r.code IN ('STORE_ADMIN', 'TENANT_ADMIN', 'PLATFORM_ADMIN')
AND p.code IN (
'product.catalog.read', 'product.catalog.write',
'inventory.read', 'inventory.adjust'
))
WHERE r.status = 'ACTIVE' AND r.deleted_at IS NULL;
INSERT IGNORE INTO qipai_schema_migrations (version, name)
VALUES ('2026081107', 'm09d1_product_inventory_foundation');
@@ -0,0 +1,203 @@
SELECT table_name
FROM information_schema.tables
WHERE table_schema = DATABASE()
AND table_name IN (
'qipai_product_categories',
'qipai_products',
'qipai_product_skus',
'qipai_product_store_listings',
'qipai_product_store_settings',
'qipai_product_store_hours',
'qipai_product_inventory',
'qipai_product_inventory_requests',
'qipai_product_inventory_ledger'
)
ORDER BY table_name;
SELECT table_name, column_name, data_type, is_nullable, generation_expression
FROM information_schema.columns
WHERE table_schema = DATABASE()
AND (
(table_name = 'qipai_product_categories'
AND column_name IN ('store_id', 'version', 'deleted_at', 'active_code_key'))
OR (table_name = 'qipai_products'
AND column_name IN (
'images_json', 'delivery_enabled', 'storage_enabled',
'version', 'deleted_at', 'active_code_key'
))
OR (table_name = 'qipai_schema_migrations'
AND column_name = 'checksum')
OR (table_name = 'qipai_product_skus'
AND column_name IN (
'sale_price_cents', 'market_price_cents', 'cost_price_cents',
'default_inventory_policy', 'version', 'deleted_at',
'active_code_key', 'active_barcode_key'
))
OR (table_name = 'qipai_product_store_listings'
AND column_name IN (
'category_id', 'fulfillment_mode', 'version', 'deleted_at', 'active_product_id'
))
OR (table_name = 'qipai_product_store_settings'
AND column_name IN (
'sales_status', 'manual_paused_at', 'manual_paused_until',
'manual_pause_reason', 'version'
))
OR (table_name = 'qipai_product_store_hours'
AND column_name IN (
'weekday', 'slot_no', 'open_minute', 'close_minute', 'crosses_midnight',
'version', 'deleted_at', 'active_slot_no'
))
OR (table_name = 'qipai_product_inventory'
AND column_name IN (
'policy_type', 'available_quantity', 'locked_quantity', 'loss_quantity',
'low_stock_threshold', 'version', 'updated_by'
))
OR (table_name = 'qipai_product_inventory_requests'
AND column_name IN (
'request_id', 'operation', 'business_type', 'business_id',
'fingerprint', 'item_count', 'operator_id', 'trace_id'
))
OR (table_name = 'qipai_product_inventory_ledger'
AND column_name IN (
'request_id', 'business_type', 'business_id', 'operation',
'available_delta', 'locked_delta', 'loss_delta',
'available_after', 'locked_after', 'loss_after', 'version_after'
))
)
ORDER BY table_name, ordinal_position;
SELECT table_name, index_name
FROM information_schema.statistics
WHERE table_schema = DATABASE()
AND index_name IN (
'uq_qipai_stores_tenant_id',
'uq_qipai_users_tenant_id',
'uq_qipai_product_category_scope_id',
'uq_qipai_product_category_active_code',
'uq_qipai_product_tenant_id',
'uq_qipai_product_active_code',
'uq_qipai_product_sku_tenant_id',
'uq_qipai_product_sku_active_code',
'uq_qipai_product_sku_active_barcode',
'uq_qipai_product_store_listing_active',
'uq_qipai_product_store_setting',
'uq_qipai_product_store_setting_scope_id',
'uq_qipai_product_store_hour_active',
'uq_qipai_product_inventory_scope',
'uq_qipai_product_inventory_scope_id',
'uq_qipai_product_inventory_request',
'uq_qipai_product_inventory_ledger_request',
'uq_qipai_product_inventory_ledger_version'
)
GROUP BY table_name, index_name
ORDER BY table_name, index_name;
SELECT constraint_name
FROM information_schema.table_constraints
WHERE constraint_schema = DATABASE()
AND constraint_type = 'CHECK'
AND constraint_name IN (
'chk_qipai_product_category_status',
'chk_qipai_product_category_version',
'chk_qipai_product_status',
'chk_qipai_product_capabilities',
'chk_qipai_product_version',
'chk_qipai_product_sku_status',
'chk_qipai_product_sku_inventory_policy',
'chk_qipai_product_sku_version',
'chk_qipai_product_sku_amounts',
'chk_qipai_product_listing_status',
'chk_qipai_product_listing_fulfillment',
'chk_qipai_product_listing_window',
'chk_qipai_product_listing_version',
'chk_qipai_product_store_setting_status',
'chk_qipai_product_store_setting_pause',
'chk_qipai_product_store_setting_version',
'chk_qipai_product_store_hour_weekday',
'chk_qipai_product_store_hour_slot',
'chk_qipai_product_store_hour_window',
'chk_qipai_product_store_hour_version',
'chk_qipai_product_inventory_policy',
'chk_qipai_product_inventory_quantity',
'chk_qipai_product_inventory_version',
'chk_qipai_product_inventory_request_operation',
'chk_qipai_product_inventory_request_items',
'chk_qipai_product_inventory_ledger_operation',
'chk_qipai_product_inventory_ledger_delta',
'chk_qipai_product_inventory_ledger_after'
)
ORDER BY constraint_name;
SELECT constraint_name
FROM information_schema.table_constraints
WHERE constraint_schema = DATABASE()
AND constraint_type = 'FOREIGN KEY'
AND constraint_name IN (
'fk_qipai_product_category_store',
'fk_qipai_product_category_parent',
'fk_qipai_product_listing_store',
'fk_qipai_product_listing_product',
'fk_qipai_product_listing_category',
'fk_qipai_product_store_setting_store',
'fk_qipai_product_store_hour_setting',
'fk_qipai_product_store_hour_store',
'fk_qipai_product_inventory_store',
'fk_qipai_product_inventory_sku',
'fk_qipai_product_inventory_request_store',
'fk_qipai_product_inventory_ledger_inventory',
'fk_qipai_product_inventory_ledger_store',
'fk_qipai_product_inventory_ledger_sku',
'fk_qipai_product_inventory_ledger_request'
)
ORDER BY constraint_name;
SELECT trigger_name, event_manipulation
FROM information_schema.triggers
WHERE trigger_schema = DATABASE()
AND trigger_name IN (
'qipai_product_inventory_ledger_no_update',
'qipai_product_inventory_ledger_no_delete'
)
ORDER BY trigger_name;
SELECT code
FROM qipai_permissions
WHERE code IN (
'product.catalog.read', 'product.catalog.write',
'inventory.read', 'inventory.adjust'
)
ORDER BY code;
SELECT COUNT(*) AS managed_role_count
FROM qipai_roles r
WHERE r.code IN ('STAFF', 'STORE_ADMIN', 'TENANT_ADMIN', 'PLATFORM_ADMIN')
AND r.status = 'ACTIVE'
AND r.deleted_at IS NULL
HAVING COUNT(*) = (
SELECT COUNT(*)
FROM (
SELECT granted_role.tenant_id, granted_role.id, granted_role.code
FROM qipai_roles granted_role
INNER JOIN qipai_role_permissions rp
ON rp.tenant_id = granted_role.tenant_id AND rp.role_id = granted_role.id
INNER JOIN qipai_permissions p ON p.id = rp.permission_id
WHERE granted_role.code IN ('STAFF', 'STORE_ADMIN', 'TENANT_ADMIN', 'PLATFORM_ADMIN')
AND granted_role.status = 'ACTIVE'
AND granted_role.deleted_at IS NULL
AND p.code IN (
'product.catalog.read', 'product.catalog.write',
'inventory.read', 'inventory.adjust'
)
GROUP BY granted_role.tenant_id, granted_role.id, granted_role.code
HAVING COUNT(DISTINCT p.code) = CASE
WHEN granted_role.code = 'STAFF' THEN 2
ELSE 4
END
) fully_granted_product_roles
);
SELECT version, name
FROM qipai_schema_migrations
WHERE version = '2026081107'
AND name = 'm09d1_product_inventory_foundation'
AND checksum REGEXP '^[0-9a-f]{64}$';