feat(M09-D1): 完成商品目录与库存流水底座
This commit is contained in:
@@ -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}$';
|
||||
Reference in New Issue
Block a user