ALTER TABLE qipai_product_order_items ADD UNIQUE KEY uq_qipai_product_order_item_scope_id (tenant_id, store_id, order_id, id); CREATE TABLE IF NOT EXISTS qipai_product_storage_records ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, tenant_id BIGINT UNSIGNED NOT NULL, store_id BIGINT UNSIGNED NOT NULL, member_id BIGINT UNSIGNED NOT NULL, source_order_id BIGINT UNSIGNED NULL, storage_no VARCHAR(64) CHARACTER SET ascii COLLATE ascii_bin NOT NULL, client_request_id VARCHAR(128) CHARACTER SET ascii COLLATE ascii_bin NOT NULL, request_fingerprint CHAR(64) CHARACTER SET ascii COLLATE ascii_bin NOT NULL, source_type VARCHAR(16) NOT NULL, status VARCHAR(32) NOT NULL DEFAULT 'STORED', item_count SMALLINT UNSIGNED NOT NULL, total_quantity BIGINT UNSIGNED NOT NULL, remaining_quantity BIGINT UNSIGNED NOT NULL, expires_at DATETIME(3) NOT NULL, claim_credential_hash CHAR(64) CHARACTER SET ascii COLLATE ascii_bin NOT NULL, claim_credential_version INT UNSIGNED NOT NULL DEFAULT 1, claim_credential_rotated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), claim_credential_consumed_at DATETIME(3) NULL, created_source VARCHAR(16) NOT NULL, created_by BIGINT UNSIGNED NULL, cancelled_at DATETIME(3) NULL, cancel_reason VARCHAR(512) NOT NULL DEFAULT '', expired_at DATETIME(3) NULL, 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), CONSTRAINT fk_qipai_product_storage_tenant FOREIGN KEY (tenant_id) REFERENCES qipai_tenants(id), CONSTRAINT fk_qipai_product_storage_store FOREIGN KEY (tenant_id, store_id) REFERENCES qipai_stores(tenant_id, id), CONSTRAINT fk_qipai_product_storage_member FOREIGN KEY (tenant_id, member_id) REFERENCES qipai_users(tenant_id, id), CONSTRAINT fk_qipai_product_storage_source_order FOREIGN KEY (tenant_id, store_id, source_order_id) REFERENCES qipai_product_orders(tenant_id, store_id, id), CONSTRAINT fk_qipai_product_storage_created_by FOREIGN KEY (tenant_id, created_by) REFERENCES qipai_users(tenant_id, id), UNIQUE KEY uq_qipai_product_storage_scope_id (tenant_id, store_id, id), UNIQUE KEY uq_qipai_product_storage_no (tenant_id, storage_no), UNIQUE KEY uq_qipai_product_storage_client_request (tenant_id, client_request_id), UNIQUE KEY uq_qipai_product_storage_source_order (tenant_id, source_order_id), KEY idx_qipai_product_storage_store_status (tenant_id, store_id, status, expires_at, id), KEY idx_qipai_product_storage_member (tenant_id, member_id, created_at, id), CONSTRAINT chk_qipai_product_storage_source CHECK ( (source_type = 'ORDER' AND source_order_id IS NOT NULL) OR (source_type = 'MANUAL' AND source_order_id IS NULL) ), CONSTRAINT chk_qipai_product_storage_status CHECK ( status IN ('STORED', 'PARTIALLY_RETRIEVED', 'RETRIEVED', 'EXPIRED', 'CANCELLED') ), CONSTRAINT chk_qipai_product_storage_counts CHECK ( item_count BETWEEN 1 AND 100 AND total_quantity BETWEEN 1 AND 1000000000 AND remaining_quantity <= total_quantity ), CONSTRAINT chk_qipai_product_storage_credential CHECK ( claim_credential_hash REGEXP '^[0-9a-f]{64}$' AND claim_credential_version >= 1 ), CONSTRAINT chk_qipai_product_storage_actor CHECK ( created_source IN ('MEMBER', 'STAFF', 'SYSTEM') AND (created_source <> 'MEMBER' OR source_type = 'ORDER') AND (created_source <> 'STAFF' OR created_by IS NOT NULL) ), CONSTRAINT chk_qipai_product_storage_version CHECK (version >= 1) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; CREATE TABLE IF NOT EXISTS qipai_product_storage_items ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, tenant_id BIGINT UNSIGNED NOT NULL, store_id BIGINT UNSIGNED NOT NULL, storage_id BIGINT UNSIGNED NOT NULL, line_no SMALLINT UNSIGNED NOT NULL, source_order_id BIGINT UNSIGNED NULL, source_order_item_id BIGINT UNSIGNED NULL, product_id BIGINT UNSIGNED NOT NULL, sku_id BIGINT UNSIGNED NOT NULL, product_code VARCHAR(64) NOT NULL, product_name VARCHAR(128) NOT NULL, sku_code VARCHAR(64) NOT NULL, sku_name VARCHAR(128) NOT NULL, unit_name VARCHAR(32) NOT NULL, attributes_snapshot JSON NULL, total_quantity BIGINT UNSIGNED NOT NULL, remaining_quantity 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_storage_item_record FOREIGN KEY (tenant_id, store_id, storage_id) REFERENCES qipai_product_storage_records(tenant_id, store_id, id), CONSTRAINT fk_qipai_product_storage_item_source FOREIGN KEY (tenant_id, store_id, source_order_id, source_order_item_id) REFERENCES qipai_product_order_items(tenant_id, store_id, order_id, id), CONSTRAINT fk_qipai_product_storage_item_product FOREIGN KEY (tenant_id, product_id) REFERENCES qipai_products(tenant_id, id), CONSTRAINT fk_qipai_product_storage_item_sku FOREIGN KEY (tenant_id, sku_id) REFERENCES qipai_product_skus(tenant_id, id), UNIQUE KEY uq_qipai_product_storage_item_line (tenant_id, store_id, storage_id, line_no), UNIQUE KEY uq_qipai_product_storage_item_source (tenant_id, source_order_item_id), UNIQUE KEY uq_qipai_product_storage_item_scope_id (tenant_id, store_id, storage_id, id), KEY idx_qipai_product_storage_item_sku (tenant_id, store_id, sku_id, created_at), CONSTRAINT chk_qipai_product_storage_item_line CHECK (line_no BETWEEN 1 AND 100), CONSTRAINT chk_qipai_product_storage_item_source CHECK ( (source_order_id IS NULL AND source_order_item_id IS NULL) OR (source_order_id IS NOT NULL AND source_order_item_id IS NOT NULL) ), CONSTRAINT chk_qipai_product_storage_item_quantity CHECK ( total_quantity BETWEEN 1 AND 1000000000 AND remaining_quantity <= total_quantity ) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; CREATE TABLE IF NOT EXISTS qipai_product_storage_events ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, tenant_id BIGINT UNSIGNED NOT NULL, store_id BIGINT UNSIGNED NOT NULL, storage_id BIGINT UNSIGNED NOT NULL, request_id VARCHAR(128) CHARACTER SET ascii COLLATE ascii_bin NOT NULL, request_fingerprint CHAR(64) CHARACTER SET ascii COLLATE ascii_bin NOT NULL, from_status VARCHAR(32) NULL, to_status VARCHAR(32) NOT NULL, action VARCHAR(32) NOT NULL, quantity BIGINT UNSIGNED NOT NULL DEFAULT 0, actor_type VARCHAR(16) NOT NULL, actor_id BIGINT UNSIGNED NULL, credential_version_after INT UNSIGNED NOT NULL, version_after INT UNSIGNED NOT NULL, reason VARCHAR(512) NOT NULL DEFAULT '', trace_id VARCHAR(128) CHARACTER SET ascii COLLATE ascii_bin NOT NULL, metadata JSON NULL, created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), CONSTRAINT fk_qipai_product_storage_event_record FOREIGN KEY (tenant_id, store_id, storage_id) REFERENCES qipai_product_storage_records(tenant_id, store_id, id), CONSTRAINT fk_qipai_product_storage_event_actor FOREIGN KEY (tenant_id, actor_id) REFERENCES qipai_users(tenant_id, id), UNIQUE KEY uq_qipai_product_storage_event_request (tenant_id, request_id), UNIQUE KEY uq_qipai_product_storage_event_version (tenant_id, store_id, storage_id, version_after), UNIQUE KEY uq_qipai_product_storage_event_scope_id (tenant_id, store_id, storage_id, id), KEY idx_qipai_product_storage_event_history (tenant_id, store_id, storage_id, created_at, id), CONSTRAINT chk_qipai_product_storage_event_fingerprint CHECK ( request_fingerprint REGEXP '^[0-9a-f]{64}$' ), CONSTRAINT chk_qipai_product_storage_event_from_status CHECK ( from_status IS NULL OR from_status IN ( 'STORED', 'PARTIALLY_RETRIEVED', 'RETRIEVED', 'EXPIRED', 'CANCELLED' ) ), CONSTRAINT chk_qipai_product_storage_event_to_status CHECK ( to_status IN ('STORED', 'PARTIALLY_RETRIEVED', 'RETRIEVED', 'EXPIRED', 'CANCELLED') ), CONSTRAINT chk_qipai_product_storage_event_action CHECK ( action IN ('STORED', 'RETRIEVED', 'CREDENTIAL_ROTATED', 'CANCELLED', 'EXPIRED') ), CONSTRAINT chk_qipai_product_storage_event_actor CHECK ( actor_type IN ('MEMBER', 'STAFF', 'SYSTEM') AND (actor_type <> 'STAFF' OR actor_id IS NOT NULL) ), CONSTRAINT chk_qipai_product_storage_event_versions CHECK ( credential_version_after >= 1 AND version_after >= 1 ) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; CREATE TABLE IF NOT EXISTS qipai_product_storage_event_items ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, tenant_id BIGINT UNSIGNED NOT NULL, store_id BIGINT UNSIGNED NOT NULL, storage_id BIGINT UNSIGNED NOT NULL, event_id BIGINT UNSIGNED NOT NULL, storage_item_id BIGINT UNSIGNED NOT NULL, line_no SMALLINT UNSIGNED NOT NULL, sku_id BIGINT UNSIGNED NOT NULL, quantity BIGINT UNSIGNED NOT NULL, remaining_after BIGINT UNSIGNED NOT NULL, created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), CONSTRAINT fk_qipai_product_storage_event_item_event FOREIGN KEY (tenant_id, store_id, storage_id, event_id) REFERENCES qipai_product_storage_events(tenant_id, store_id, storage_id, id), CONSTRAINT fk_qipai_product_storage_event_item_storage_item FOREIGN KEY (tenant_id, store_id, storage_id, storage_item_id) REFERENCES qipai_product_storage_items(tenant_id, store_id, storage_id, id), CONSTRAINT fk_qipai_product_storage_event_item_sku FOREIGN KEY (tenant_id, sku_id) REFERENCES qipai_product_skus(tenant_id, id), UNIQUE KEY uq_qipai_product_storage_event_item_line (tenant_id, store_id, storage_id, event_id, line_no), KEY idx_qipai_product_storage_event_item_storage (tenant_id, store_id, storage_id, storage_item_id, created_at), CONSTRAINT chk_qipai_product_storage_event_item_quantity CHECK ( line_no BETWEEN 1 AND 100 AND quantity BETWEEN 1 AND 1000000000 AND remaining_after <= 1000000000 ) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; DROP TRIGGER IF EXISTS qipai_product_storage_events_no_update; CREATE TRIGGER qipai_product_storage_events_no_update BEFORE UPDATE ON qipai_product_storage_events FOR EACH ROW SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'PRODUCT_STORAGE_EVENT_IMMUTABLE'; DROP TRIGGER IF EXISTS qipai_product_storage_events_no_delete; CREATE TRIGGER qipai_product_storage_events_no_delete BEFORE DELETE ON qipai_product_storage_events FOR EACH ROW SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'PRODUCT_STORAGE_EVENT_IMMUTABLE'; DROP TRIGGER IF EXISTS qipai_product_storage_event_items_no_update; CREATE TRIGGER qipai_product_storage_event_items_no_update BEFORE UPDATE ON qipai_product_storage_event_items FOR EACH ROW SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'PRODUCT_STORAGE_EVENT_ITEM_IMMUTABLE'; DROP TRIGGER IF EXISTS qipai_product_storage_event_items_no_delete; CREATE TRIGGER qipai_product_storage_event_items_no_delete BEFORE DELETE ON qipai_product_storage_event_items FOR EACH ROW SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'PRODUCT_STORAGE_EVENT_ITEM_IMMUTABLE'; INSERT IGNORE INTO qipai_permissions (code, name, category) VALUES ('goods.storage.read', '查看商品寄存', 'goods'), ('goods.storage.manage', '处理商品寄存', 'goods'); 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 p.code IN ('goods.storage.read', 'goods.storage.manage') WHERE r.code IN ('STAFF', 'STORE_ADMIN', 'TENANT_ADMIN', 'PLATFORM_ADMIN') AND r.status = 'ACTIVE' AND r.deleted_at IS NULL; INSERT IGNORE INTO qipai_schema_migrations (version, name) VALUES ('2026081109', 'm09d3_product_storage');