-- Firstleg -> Finance 费用来源事件和柜次费用模型(MySQL 8,可重复执行) DROP PROCEDURE IF EXISTS firstleg_finance_add_column; DELIMITER $$ CREATE PROCEDURE firstleg_finance_add_column( IN target_table VARCHAR(64), IN target_column VARCHAR(64), IN column_definition TEXT ) BEGIN IF NOT EXISTS ( SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = target_table AND column_name = target_column ) THEN SET @ddl = CONCAT('ALTER TABLE `', target_table, '` ADD COLUMN `', target_column, '` ', column_definition); PREPARE statement FROM @ddl; EXECUTE statement; DEALLOCATE PREPARE statement; END IF; END$$ DELIMITER ; CALL firstleg_finance_add_column('fl_inbound_outbox', 'event_key', "VARCHAR(160) NULL COMMENT '费用来源事件幂等键;历史事件允许为空' AFTER event_type"); CALL firstleg_finance_add_column('fl_inbound_outbox', 'event_version', "INT UNSIGNED NOT NULL DEFAULT 1 COMMENT '事件信封版本' AFTER event_key"); CALL firstleg_finance_add_column('fl_inbound_outbox', 'aggregate_sequence', "INT UNSIGNED NULL COMMENT '同一费用来源的单调版本序号' AFTER event_version"); CALL firstleg_finance_add_column('fl_inbound_outbox', 'payload_hash', "CHAR(64) NULL COMMENT '短payload规范化JSON的SHA-256' AFTER aggregate_sequence"); CALL firstleg_finance_add_column('fl_inbound_outbox', 'next_retry_at', "DATETIME(3) NULL COMMENT '下一次允许投递时间' AFTER retry_count"); CALL firstleg_finance_add_column('fl_inbound_outbox', 'published_at', "DATETIME(3) NULL COMMENT 'Finance确认接收时间' AFTER next_retry_at"); CALL firstleg_finance_add_column('fl_inbound_outbox', 'locked_at', "DATETIME(3) NULL COMMENT 'Relay抢占租约时间' AFTER published_at"); CALL firstleg_finance_add_column('fl_inbound_outbox', 'locked_by', "VARCHAR(64) NULL COMMENT 'Relay抢占实例ID' AFTER locked_at"); CALL firstleg_finance_add_column('fl_inbound_outbox', 'last_error', "VARCHAR(500) NULL COMMENT '最近一次投递失败摘要,不记录令牌' AFTER locked_by"); DROP PROCEDURE IF EXISTS firstleg_finance_add_index; DELIMITER $$ CREATE PROCEDURE firstleg_finance_add_index( IN target_table VARCHAR(64), IN target_index VARCHAR(64), IN index_definition TEXT ) BEGIN IF NOT EXISTS ( SELECT 1 FROM information_schema.statistics WHERE table_schema = DATABASE() AND table_name = target_table AND index_name = target_index ) THEN SET @ddl = CONCAT('ALTER TABLE `', target_table, '` ADD ', index_definition); PREPARE statement FROM @ddl; EXECUTE statement; DEALLOCATE PREPARE statement; END IF; END$$ DELIMITER ; CALL firstleg_finance_add_index('fl_inbound_outbox', 'uk_inbound_outbox_event_key', 'UNIQUE KEY `uk_inbound_outbox_event_key` (`event_key`)'); CALL firstleg_finance_add_index('fl_inbound_outbox', 'idx_inbound_outbox_finance_relay', 'KEY `idx_inbound_outbox_finance_relay` (`publish_status`, `next_retry_at`, `created_at`, `id`)'); DROP PROCEDURE IF EXISTS firstleg_finance_add_index; CREATE TABLE IF NOT EXISTS fl_container_fee_detail ( id BIGINT UNSIGNED NOT NULL COMMENT '柜次费用来源ID', tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', loading_plan_id BIGINT UNSIGNED NOT NULL COMMENT '柜次ID,对应fl_loading_plan.id', container_booking_id BIGINT UNSIGNED NULL COMMENT '订舱柜ID快照', container_no VARCHAR(64) NULL COMMENT '柜号快照', fee_item_id BIGINT UNSIGNED NOT NULL COMMENT '平台公共费用项目ID', fee_item_code_snapshot VARCHAR(50) NOT NULL COMMENT '费用项目稳定编码快照', fee_item_name_snapshot VARCHAR(128) NOT NULL COMMENT '费用项目名称快照', direction VARCHAR(16) NOT NULL COMMENT '费用方向:RECEIVABLE应收、PAYABLE应付', supplier_id BIGINT UNSIGNED NULL COMMENT '应付Warehouse供应商ID快照;应收为空', supplier_name VARCHAR(200) NULL COMMENT '应付Warehouse供应商名称快照;应收为空', supplier_category VARCHAR(16) NULL COMMENT '费用项目供应商类别约束快照;为空表示任意启用供应商', business_date DATE NOT NULL COMMENT '费用业务日期', amount DECIMAL(18,2) NOT NULL COMMENT 'CNY费用金额', currency CHAR(3) NOT NULL DEFAULT 'CNY' COMMENT '币种,首期固定CNY', review_status VARCHAR(16) NOT NULL DEFAULT 'PENDING' COMMENT '审核状态:PENDING待审核、APPROVED通过、REJECTED驳回', source_status VARCHAR(16) NOT NULL DEFAULT 'ACTIVE' COMMENT '来源状态:ACTIVE有效、VOIDED作废', source_version INT UNSIGNED NOT NULL DEFAULT 1 COMMENT '费用来源版本;调整递增,作废携带当前版本', remark VARCHAR(500) NULL COMMENT '人工备注', reviewer_id BIGINT UNSIGNED NULL COMMENT '审核人ID', reviewer_name VARCHAR(64) NULL COMMENT '审核人名称快照', reviewed_at DATETIME(3) NULL COMMENT '审核时间', review_reason VARCHAR(500) NULL COMMENT '审核驳回原因', created_by BIGINT UNSIGNED NOT NULL COMMENT '创建人ID', created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间', updated_by BIGINT UNSIGNED NOT NULL COMMENT '最后更新人ID', updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '最后更新时间', PRIMARY KEY (id), KEY idx_container_fee_customer_plan (tenant_id, loading_plan_id, source_status), KEY idx_container_fee_review (tenant_id, review_status, source_status), CONSTRAINT chk_container_fee_direction CHECK (direction IN ('RECEIVABLE', 'PAYABLE')), CONSTRAINT chk_container_fee_currency CHECK (currency = 'CNY'), CONSTRAINT chk_container_fee_amount CHECK (amount >= 0), CONSTRAINT chk_container_fee_review CHECK (review_status IN ('PENDING', 'APPROVED', 'REJECTED')), CONSTRAINT chk_container_fee_source CHECK (source_status IN ('ACTIVE', 'VOIDED')) ) ENGINE=InnoDB COMMENT='柜次级人工应收应付费用来源;审核通过后自动同步Finance'; CREATE TABLE IF NOT EXISTS fl_container_fee_source_snapshot ( id BIGINT UNSIGNED NOT NULL COMMENT '柜次费用快照ID', source_fee_id BIGINT UNSIGNED NOT NULL COMMENT '柜次费用来源ID', source_version INT UNSIGNED NOT NULL COMMENT '费用来源版本', tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', loading_plan_id BIGINT UNSIGNED NOT NULL COMMENT '柜次ID', container_booking_id BIGINT UNSIGNED NULL COMMENT '订舱柜ID快照', container_no VARCHAR(64) NULL COMMENT '柜号快照', fee_item_id BIGINT UNSIGNED NOT NULL COMMENT '费用项目ID快照', fee_item_code VARCHAR(50) NOT NULL COMMENT '费用项目编码快照', fee_item_name VARCHAR(128) NOT NULL COMMENT '费用项目名称快照', direction VARCHAR(16) NOT NULL COMMENT '费用方向', supplier_id BIGINT UNSIGNED NULL COMMENT 'Warehouse供应商ID快照', supplier_name VARCHAR(200) NULL COMMENT 'Warehouse供应商名称快照', supplier_category VARCHAR(16) NULL COMMENT '费用项目供应商类别约束快照;为空表示任意启用供应商', business_date DATE NOT NULL COMMENT '费用业务日期', amount DECIMAL(18,2) NOT NULL COMMENT 'CNY费用金额', currency CHAR(3) NOT NULL DEFAULT 'CNY' COMMENT '币种,首期固定CNY', review_status VARCHAR(16) NOT NULL COMMENT '审核状态快照', reviewed_at DATETIME(3) NULL COMMENT '审核时间快照', created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '快照创建时间', PRIMARY KEY (id), UNIQUE KEY uk_container_fee_source_version (loading_plan_id, source_fee_id, source_version), KEY idx_container_fee_snapshot_tenant (tenant_id, loading_plan_id) ) ENGINE=InnoDB COMMENT='柜次费用不可变版本快照'; CALL firstleg_finance_add_column('fl_container_fee_detail', 'supplier_id', "BIGINT UNSIGNED NULL COMMENT '应付Warehouse供应商ID快照;应收为空' AFTER direction"); CALL firstleg_finance_add_column('fl_container_fee_detail', 'supplier_name', "VARCHAR(200) NULL COMMENT '应付Warehouse供应商名称快照;应收为空' AFTER supplier_id"); CALL firstleg_finance_add_column('fl_container_fee_detail', 'supplier_category', "VARCHAR(16) NULL COMMENT '费用项目供应商类别约束快照;为空表示任意启用供应商' AFTER supplier_name"); CALL firstleg_finance_add_column('fl_inbound_fee_detail', 'supplier_id', "BIGINT UNSIGNED NULL COMMENT '应付Warehouse供应商ID快照;应收为空' AFTER direction"); CALL firstleg_finance_add_column('fl_inbound_fee_detail', 'supplier_name', "VARCHAR(200) NULL COMMENT '应付Warehouse供应商名称快照;应收为空' AFTER supplier_id"); CALL firstleg_finance_add_column('fl_inbound_fee_detail', 'supplier_category', "VARCHAR(16) NULL COMMENT '费用项目供应商类别约束快照;为空表示任意启用供应商' AFTER supplier_name"); DROP PROCEDURE IF EXISTS firstleg_finance_add_column; DROP PROCEDURE IF EXISTS firstleg_finance_assert_schema; DELIMITER $$ CREATE PROCEDURE firstleg_finance_assert_schema() BEGIN IF (SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'fl_inbound_outbox' AND column_name IN ('event_key', 'event_version', 'aggregate_sequence', 'payload_hash', 'next_retry_at', 'published_at', 'locked_at', 'locked_by', 'last_error')) <> 9 THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Firstleg Finance迁移失败:Outbox事件信封字段不完整'; END IF; IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'fl_container_fee_detail') THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Firstleg Finance迁移失败:柜次费用表不存在'; END IF; IF (SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'fl_container_fee_detail' AND column_name IN ('supplier_id', 'supplier_name', 'supplier_category')) <> 3 THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Firstleg Finance迁移失败:柜次费用供应商快照字段不完整'; END IF; IF (SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'fl_inbound_fee_detail' AND column_name IN ('supplier_id', 'supplier_name', 'supplier_category')) <> 3 THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Firstleg Finance迁移失败:入库费用供应商快照字段不完整'; END IF; END$$ DELIMITER ; CALL firstleg_finance_assert_schema(); DROP PROCEDURE IF EXISTS firstleg_finance_assert_schema;