diff --git a/deploy/sql/头程数据库统一增量-MySQL8.sql b/deploy/sql/头程数据库统一增量-MySQL8.sql index c9402cdb2..5c7adf4f8 100644 --- a/deploy/sql/头程数据库统一增量-MySQL8.sql +++ b/deploy/sql/头程数据库统一增量-MySQL8.sql @@ -3054,7 +3054,7 @@ CREATE TABLE IF NOT EXISTS fl_inbound_fee ( subtotal_amount DECIMAL(18,2) NOT NULL COMMENT '基础费用金额', additional_amount DECIMAL(18,2) NOT NULL DEFAULT 0 COMMENT '附加费金额', total_amount DECIMAL(18,2) NOT NULL COMMENT '总金额', - fee_status VARCHAR(24) NOT NULL DEFAULT 'CALCULATED' COMMENT '费用状态:CALCULATED已计算、SUPERSEDED已被新版本替代', + fee_status VARCHAR(24) NOT NULL DEFAULT 'CALCULATED' COMMENT '费用状态:CALCULATED已计算、CONFIRMED已确认、SENT已发送、SUPERSEDED已被新版本替代、VOIDED已作废', review_status VARCHAR(24) NOT NULL DEFAULT 'NOT_SUBMITTED' COMMENT '费用审核状态:NOT_SUBMITTED未提交、PENDING待审核、APPROVED已通过、REJECTED已驳回、CONFIRMED已确认', exception_release TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否基于测量异常放行生成', calculated_by BIGINT UNSIGNED NOT NULL COMMENT '试算人ID', @@ -11235,6 +11235,32 @@ CREATE TABLE IF NOT EXISTS fl_container_fee_source_snapshot ( KEY idx_container_fee_snapshot_tenant (tenant_id, loading_plan_id) ) ENGINE=InnoDB COMMENT='柜次费用不可变版本快照'; +CREATE TABLE IF NOT EXISTS fl_finance_fee_source_command ( + id BIGINT UNSIGNED NOT NULL COMMENT 'Finance费用来源命令ID', + customer_id BIGINT UNSIGNED NOT NULL COMMENT '客户隔离键,由Firstleg费用事实再次校验', + operator_id BIGINT UNSIGNED NOT NULL COMMENT 'Finance操作人ID', + operator_name VARCHAR(64) NOT NULL COMMENT 'Finance操作人名称快照', + action VARCHAR(24) NOT NULL COMMENT '命令动作;当前仅VOID', + idempotency_key VARCHAR(128) NOT NULL COMMENT 'Finance透传的幂等键', + request_hash CHAR(64) NOT NULL COMMENT '规范化请求SHA-256', + source_type VARCHAR(24) NOT NULL COMMENT 'INBOUND_FEE或CONTAINER_FEE', + source_fee_id BIGINT UNSIGNED NOT NULL COMMENT '稳定费用来源ID', + source_version INT UNSIGNED NOT NULL COMMENT '精确费用来源版本', + inbound_id BIGINT UNSIGNED NULL COMMENT '入库费用所属入库单ID', + container_id BIGINT UNSIGNED NULL COMMENT '柜次费用所属柜次ID', + reason VARCHAR(500) NOT NULL COMMENT '操作原因', + status VARCHAR(16) NOT NULL COMMENT 'PROCESSING或SUCCEEDED', + result_status VARCHAR(16) NULL COMMENT '成功业务结果;当前为VOIDED', + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '命令创建时间', + completed_at DATETIME(3) NULL COMMENT '命令成功完成时间', + PRIMARY KEY (id), + UNIQUE KEY uk_finance_fee_command_idem (customer_id, operator_id, action, idempotency_key), + KEY idx_finance_fee_command_source (source_type, source_fee_id, source_version, created_at), + CONSTRAINT chk_finance_fee_command_action CHECK (action IN ('VOID')), + CONSTRAINT chk_finance_fee_command_status CHECK (status IN ('PROCESSING', 'SUCCEEDED')), + CONSTRAINT chk_finance_fee_command_source CHECK (source_type IN ('INBOUND_FEE', 'CONTAINER_FEE')) +) ENGINE=InnoDB COMMENT='Finance发起、Firstleg执行业务费用命令的幂等审计记录'; + 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', @@ -11263,6 +11289,10 @@ BEGIN WHERE table_schema = DATABASE() AND table_name = 'fl_container_fee_detail') THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Firstleg Finance迁移失败:柜次费用表不存在'; END IF; + IF NOT EXISTS (SELECT 1 FROM information_schema.tables + WHERE table_schema = DATABASE() AND table_name = 'fl_finance_fee_source_command') 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 @@ -12633,7 +12663,7 @@ CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee', 'destination_type CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee', 'destination_warehouse_id', '海外仓或FBA目的仓ID快照'); CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee', 'exception_release', '是否基于测量异常放行生成'); CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee', 'fee_no', '客户内唯一费用单号'); -CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee', 'fee_status', '费用状态:CALCULATED已计算、SUPERSEDED已被新版本替代'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee', 'fee_status', '费用状态:CALCULATED已计算、CONFIRMED已确认、SENT已发送、SUPERSEDED已被新版本替代、VOIDED已作废'); CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee', 'fee_version', '费用快照版本'); CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee', 'id', '入库费用ID'); CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee', 'inbound_id', '入库单ID'); diff --git a/firstleg/src/main/resources/db/finance-fee-source-migration.sql b/firstleg/src/main/resources/db/finance-fee-source-migration.sql index 1e773c1e7..78018fb67 100644 --- a/firstleg/src/main/resources/db/finance-fee-source-migration.sql +++ b/firstleg/src/main/resources/db/finance-fee-source-migration.sql @@ -141,8 +141,8 @@ CREATE TABLE IF NOT EXISTS fl_finance_fee_source_command ( reason VARCHAR(500) NOT NULL COMMENT '操作原因', status VARCHAR(16) NOT NULL COMMENT 'PROCESSING或SUCCEEDED', result_status VARCHAR(16) NULL COMMENT '成功业务结果;当前为VOIDED', - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - completed_at DATETIME(3) NULL, + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '命令创建时间', + completed_at DATETIME(3) NULL COMMENT '命令成功完成时间', PRIMARY KEY (id), UNIQUE KEY uk_finance_fee_command_idem (customer_id, operator_id, action, idempotency_key), KEY idx_finance_fee_command_source (source_type, source_fee_id, source_version, created_at), diff --git a/firstleg/src/test/java/com/baiyue/firstleg/service/FinanceFeeSourceCommandServiceTest.java b/firstleg/src/test/java/com/baiyue/firstleg/service/FinanceFeeSourceCommandServiceTest.java index d6f0abd28..772e4f0ad 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/service/FinanceFeeSourceCommandServiceTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/service/FinanceFeeSourceCommandServiceTest.java @@ -50,7 +50,7 @@ class FinanceFeeSourceCommandServiceTest { objectMapper = new ObjectMapper().findAndRegisterModules(); service = new FinanceFeeSourceCommandService(commands, inboundFees, pricing, containerFees, containerFeeService, outbox, objectMapper); - doAnswer(invocation -> { + lenient().doAnswer(invocation -> { FinanceFeeSourceCommandEntity value = invocation.getArgument(0); value.setId(901L); return null; @@ -80,13 +80,13 @@ class FinanceFeeSourceCommandServiceTest { VoidRequest request = inboundRequest(700L, 3); when(inboundFees.findSourceVersion(100L, 700L, 3)) .thenReturn(inboundDetail("FBA_PRICING", 778L, 700L, 3, 9L)); - when(pricing.voidSentFee(9L, 100L, 700L, 3, any())).thenReturn(1); + when(pricing.voidSentFee(eq(9L), eq(100L), eq(700L), eq(3), any())).thenReturn(1); when(pricing.markInboundFeeVoided(9L, 100L)).thenReturn(1); when(inboundFees.voidApproved(9L, 100L, 778L, 3, 71L)).thenReturn(1); service.voidSource("void-fba", hash(request), request); - verify(pricing).voidSentFee(9L, 100L, 700L, 3, any()); + verify(pricing).voidSentFee(eq(9L), eq(100L), eq(700L), eq(3), any()); verify(pricing).markInboundFeeVoided(9L, 100L); verify(outbox).publishInbound(org.mockito.ArgumentMatchers.eq(FinanceFeeSourceOutboxService.VOIDED), org.mockito.ArgumentMatchers.eq(9L), org.mockito.ArgumentMatchers.eq(100L),