diff --git a/finance/src/main/java/com/baiyue/finance/service/FinanceDocumentNumberGenerator.java b/finance/src/main/java/com/baiyue/finance/service/FinanceDocumentNumberGenerator.java index 259ba7a6e..2a98556fc 100644 --- a/finance/src/main/java/com/baiyue/finance/service/FinanceDocumentNumberGenerator.java +++ b/finance/src/main/java/com/baiyue/finance/service/FinanceDocumentNumberGenerator.java @@ -1,5 +1,6 @@ package com.baiyue.finance.service; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Propagation; @@ -22,6 +23,7 @@ public class FinanceDocumentNumberGenerator { private final JdbcTemplate jdbc; private final Clock clock; + @Autowired public FinanceDocumentNumberGenerator(JdbcTemplate jdbc) { this(jdbc, Clock.system(BUSINESS_ZONE)); } diff --git a/finance/src/main/resources/db/fee-source-event-migration.sql b/finance/src/main/resources/db/fee-source-event-migration.sql index fe7799e12..03ee5011f 100644 --- a/finance/src/main/resources/db/fee-source-event-migration.sql +++ b/finance/src/main/resources/db/fee-source-event-migration.sql @@ -41,6 +41,14 @@ CALL finance_fee_source_add_column('fin_fee_source', 'operation_version', "INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '费用编辑、提交、审核、调整和作废操作版本号' AFTER review_reason"); CALL finance_fee_source_add_column('fin_fee_source', 'updated_at', "DATETIME(3) NULL COMMENT '来源状态最后更新时间,数据修改时自动刷新' AFTER created_at"); +CALL finance_fee_source_add_column('fin_bill', 'customer_confirmation_status', + "VARCHAR(24) NOT NULL DEFAULT 'NOT_REQUIRED' COMMENT '客户确认状态:NOT_REQUIRED无需确认、PENDING_CONFIRMATION待确认、CONFIRMED已确认' AFTER issued_by"); +CALL finance_fee_source_add_column('fin_bill', 'customer_confirmed_by', + "BIGINT UNSIGNED NULL COMMENT '客户确认人用户ID' AFTER customer_confirmation_status"); +CALL finance_fee_source_add_column('fin_bill', 'customer_confirmed_by_name', + "VARCHAR(128) NULL COMMENT '客户确认人姓名快照' AFTER customer_confirmed_by"); +CALL finance_fee_source_add_column('fin_bill', 'customer_confirmed_at', + "DATETIME(3) NULL COMMENT '客户确认时间' AFTER customer_confirmed_by_name"); CALL finance_fee_source_add_column('fin_inbox_event', 'payload_hash', "CHAR(64) NULL COMMENT '规范化事件内容SHA-256哈希,用于同事件ID冲突检测' AFTER payload"); CALL finance_fee_source_add_column('fin_inbox_event', 'result_resource_id', @@ -91,6 +99,12 @@ BEGIN AND column_name IN ('payload_hash','result_resource_id','error_code')) <> 3 THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Finance费用来源迁移失败:Inbox幂等字段不完整'; END IF; + IF (SELECT COUNT(*) FROM information_schema.columns + WHERE table_schema = DATABASE() AND table_name = 'fin_bill' + AND column_name IN ('customer_confirmation_status','customer_confirmed_by', + 'customer_confirmed_by_name','customer_confirmed_at')) <> 4 THEN + SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Finance客户对账单确认字段不完整'; + END IF; END$$ DELIMITER ; CALL finance_fee_source_assert_schema(); diff --git a/finance/src/test/java/com/baiyue/finance/service/FinanceDocumentNumberGeneratorTest.java b/finance/src/test/java/com/baiyue/finance/service/FinanceDocumentNumberGeneratorTest.java index 19c00dda8..26720351a 100644 --- a/finance/src/test/java/com/baiyue/finance/service/FinanceDocumentNumberGeneratorTest.java +++ b/finance/src/test/java/com/baiyue/finance/service/FinanceDocumentNumberGeneratorTest.java @@ -1,6 +1,7 @@ package com.baiyue.finance.service; import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.jdbc.core.JdbcTemplate; import java.time.Clock; @@ -18,6 +19,18 @@ class FinanceDocumentNumberGeneratorTest { private static final Clock CLOCK = Clock.fixed( Instant.parse("2026-08-18T02:00:00Z"), ZoneId.of("Asia/Shanghai")); + @Test + void startsAsASpringBeanWhenTheTestClockConstructorAlsoExists() { + JdbcTemplate jdbc = mock(JdbcTemplate.class); + try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext()) { + context.registerBean(JdbcTemplate.class, () -> jdbc); + context.register(FinanceDocumentNumberGenerator.class); + context.refresh(); + + assertThat(context.getBean(FinanceDocumentNumberGenerator.class)).isNotNull(); + } + } + @Test void formatsShortDailySequenceForAllThreeDocuments() { JdbcTemplate jdbc = mock(JdbcTemplate.class); diff --git a/firstleg/src/main/java/com/baiyue/firstleg/service/FirstlegDocumentNumberGenerator.java b/firstleg/src/main/java/com/baiyue/firstleg/service/FirstlegDocumentNumberGenerator.java index f1e20a633..d69d567cf 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/service/FirstlegDocumentNumberGenerator.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/service/FirstlegDocumentNumberGenerator.java @@ -2,6 +2,7 @@ package com.baiyue.firstleg.service; import com.baiyue.common.core.api.ResultCode; import com.baiyue.common.core.exception.BizException; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Propagation; @@ -24,6 +25,7 @@ public class FirstlegDocumentNumberGenerator { private final JdbcTemplate jdbc; private final Clock clock; + @Autowired public FirstlegDocumentNumberGenerator(JdbcTemplate jdbc) { this(jdbc, Clock.system(BUSINESS_ZONE)); } 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 0e6712948..f5aea388d 100644 --- a/firstleg/src/main/resources/db/finance-fee-source-migration.sql +++ b/firstleg/src/main/resources/db/finance-fee-source-migration.sql @@ -73,7 +73,7 @@ CREATE TABLE IF NOT EXISTS fl_container_fee_detail ( 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 '费用项目供应商类别快照;应收为空', + 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', @@ -100,37 +100,43 @@ CREATE TABLE IF NOT EXISTS fl_container_fee_detail ( ) ENGINE=InnoDB COMMENT='柜次级人工应收应付费用来源;审核通过后自动同步Finance'; CREATE TABLE IF NOT EXISTS fl_container_fee_source_snapshot ( - id BIGINT UNSIGNED NOT NULL, - source_fee_id BIGINT UNSIGNED NOT NULL, - source_version INT UNSIGNED NOT NULL, - tenant_id BIGINT UNSIGNED NOT NULL, - loading_plan_id BIGINT UNSIGNED NOT NULL, - container_booking_id BIGINT UNSIGNED NULL, - container_no VARCHAR(64) NULL, - fee_item_id BIGINT UNSIGNED NOT NULL, - fee_item_code VARCHAR(50) NOT NULL, - fee_item_name VARCHAR(128) NOT NULL, - direction VARCHAR(16) NOT NULL, - supplier_id BIGINT UNSIGNED NULL, - supplier_name VARCHAR(200) NULL, - supplier_category VARCHAR(16) NULL, - business_date DATE NOT NULL, - amount DECIMAL(18,2) NOT NULL, - currency CHAR(3) NOT NULL DEFAULT 'CNY', - review_status VARCHAR(16) NOT NULL, - reviewed_at DATETIME(3) NULL, - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + 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"); +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; diff --git a/firstleg/src/test/java/com/baiyue/firstleg/service/FirstlegDocumentNumberGeneratorTest.java b/firstleg/src/test/java/com/baiyue/firstleg/service/FirstlegDocumentNumberGeneratorTest.java index 5b0e1af0a..911518b9c 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/service/FirstlegDocumentNumberGeneratorTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/service/FirstlegDocumentNumberGeneratorTest.java @@ -2,6 +2,7 @@ package com.baiyue.firstleg.service; import com.baiyue.common.core.exception.BizException; import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.jdbc.core.JdbcTemplate; import java.time.Clock; @@ -19,6 +20,18 @@ class FirstlegDocumentNumberGeneratorTest { private static final Clock CLOCK = Clock.fixed( Instant.parse("2026-08-18T02:00:00Z"), ZoneId.of("Asia/Shanghai")); + @Test + void startsAsASpringBeanWhenTheTestClockConstructorAlsoExists() { + JdbcTemplate jdbc = mock(JdbcTemplate.class); + try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext()) { + context.registerBean(JdbcTemplate.class, () -> jdbc); + context.register(FirstlegDocumentNumberGenerator.class); + context.refresh(); + + assertThat(context.getBean(FirstlegDocumentNumberGenerator.class)).isNotNull(); + } + } + @Test void formatsAllOperationalDocumentPrefixes() { JdbcTemplate jdbc = mock(JdbcTemplate.class); diff --git a/iam/src/test/java/com/baiyue/iam/repository/FinanceG3G4PermissionSqlContractTest.java b/iam/src/test/java/com/baiyue/iam/repository/FinanceG3G4PermissionSqlContractTest.java index 8463aa869..51182e2aa 100644 --- a/iam/src/test/java/com/baiyue/iam/repository/FinanceG3G4PermissionSqlContractTest.java +++ b/iam/src/test/java/com/baiyue/iam/repository/FinanceG3G4PermissionSqlContractTest.java @@ -51,7 +51,7 @@ class FinanceG3G4PermissionSqlContractTest { assertThat(sql) .contains("platform:finance:settlement-ar:list") .contains("platform:finance:settlement-ap:list") - .contains("permission_code LIKE 'platform:finance:%' AND status='ACTIVE' AND deleted=0) <> 27") + .contains("permission_code LIKE 'platform:finance:%' AND status='ACTIVE' AND deleted=0) <> 32") .contains("rp.role_id=4000001") .contains("LIKE 'platform:finance:%')) <> 28"); } diff --git a/scripts/admin-test-env-modules/finance-extended-settlement-contract.test.mjs b/scripts/admin-test-env-modules/finance-extended-settlement-contract.test.mjs index 3a93dec5d..576335d13 100644 --- a/scripts/admin-test-env-modules/finance-extended-settlement-contract.test.mjs +++ b/scripts/admin-test-env-modules/finance-extended-settlement-contract.test.mjs @@ -49,5 +49,6 @@ test('扩展财务验收输出可回传资源与柜次部署阻断', () => { test('按柜 PAYABLE 费用携带 Warehouse 供应商', () => { assert.match(fixtureSource, /supplierId: id\(supplier\.id\)/) assert.doesNotMatch(fixtureSource, /partnerId|partnerName/) - assert.match(fixtureSource, /\/api\/platform\/v1\/suppliers\?page=1&size=200&status=ACTIVE&category=BOOKING/) + assert.match(fixtureSource, /POST', '\/api\/platform\/v1\/suppliers\?tenantId=0'/) + assert.match(fixtureSource, /!category \|\| detail\?\.category === category/) }) diff --git a/scripts/admin-test-env-modules/finance-full-fixtures-contract.test.mjs b/scripts/admin-test-env-modules/finance-full-fixtures-contract.test.mjs index aa12bef4d..30cf9d1e0 100644 --- a/scripts/admin-test-env-modules/finance-full-fixtures-contract.test.mjs +++ b/scripts/admin-test-env-modules/finance-full-fixtures-contract.test.mjs @@ -47,7 +47,7 @@ test('Finance PAYABLE fixture creates an isolated ACTIVE supplier through the fo assert.equal(body.status, 'ACTIVE') assert.match(body.name, /^财务验收供应商-/) assert.match(source, /POST', '\/api\/platform\/v1\/suppliers\?tenantId=0'/) - assert.match(source, /detail\?\.status === 'ACTIVE' && detail\?\.category === category/) + assert.match(source, /detail\?\.status === 'ACTIVE' && \(!category \|\| detail\?\.category === category\)/) }) test('fee detail writes preserve 19-digit fee item IDs as strings', () => { diff --git a/scripts/admin-test-env-modules/finance-reconciliation-full-contract.test.mjs b/scripts/admin-test-env-modules/finance-reconciliation-full-contract.test.mjs index 73870ab9e..212c05c4b 100644 --- a/scripts/admin-test-env-modules/finance-reconciliation-full-contract.test.mjs +++ b/scripts/admin-test-env-modules/finance-reconciliation-full-contract.test.mjs @@ -253,7 +253,7 @@ test('run 发布一条日期链、三张独立已出账单、三张 AR 和一张 assert.equal(fixture.payableSettlements.length, 1) assert.equal(fixture.payableSettlements[0].supplierId, '501') assert.equal(fixture.payableSettlements[0].supplierId, '501') - assert.equal(fixture.payableSettlements[0].supplierName, 'Fixture Partner') + assert.equal(fixture.payableSettlements[0].supplierName, 'Fixture Supplier') assert.equal(context.coverageGaps.some((gap) => gap.category === 'incomplete-container-cost-attempt' && gap.reason.includes('containerBookingId=CB-FAILED') && gap.reason.includes('containerBookingId=CB-COST-RESPONSE')), true)