diff --git a/deploy/sql/README.md b/deploy/sql/README.md index c4cbee23e..59d5e1f5f 100644 --- a/deploy/sql/README.md +++ b/deploy/sql/README.md @@ -116,6 +116,12 @@ SELECT COUNT(*) = 8 AS scheduler_columns_ready - 新增 `external_request_hash`、`inbound_detail_id`、`external_detail_no` 和第三方明细唯一索引,用于一条回传绑定一条订单货物明细及一张文件中心图片。 - 部署 Firstleg 新版本前必须完整执行固定入口,并确认末尾总体验证为 `PASS`。 +## 稳定箱号与单箱当前测量 + +- 固定入口已包含 `fl_inbound_carton_label`,首次打印时按订单货物明细冻结每个实际箱的 `box_code` 和箱下标;重复打印、部分补打及设备测量均复用该映射。 +- `fl_inbound_measurement_item` 已补齐 `box_code`、`machine_no`、`device_measured_at`、`updated_at`,并通过 `uk_measure_box_code (tenant_id, box_code)` 保证同一客户下每个箱号只有一条当前测量记录。 +- 部署 Firstleg 新版本前必须完整执行固定入口。末尾会强校验映射表、四个测量字段及三个唯一约束,任一缺失均不会返回 `PASS`。 + ## 当前主体字段语义核验 - 业务单租户阶段,业务表中的 `tenant_id > 0` 是客户 ID 和客户隔离键,不表示上级租户;费用项目和入库费用明细沿用该物理字段名。 diff --git a/deploy/sql/头程数据库统一增量-MySQL8.sql b/deploy/sql/头程数据库统一增量-MySQL8.sql index bc33156b2..f4189c224 100644 --- a/deploy/sql/头程数据库统一增量-MySQL8.sql +++ b/deploy/sql/头程数据库统一增量-MySQL8.sql @@ -2440,6 +2440,7 @@ CREATE TABLE IF NOT EXISTS fl_inbound_measurement_item ( measurement_id BIGINT UNSIGNED NOT NULL COMMENT '测量批次ID', inbound_detail_id BIGINT UNSIGNED NULL COMMENT '绑定的订单货物明细ID,平台手工明细可为空', external_detail_no VARCHAR(128) NULL COMMENT '第三方测量明细唯一号,平台手工明细可为空', + box_code VARCHAR(128) NULL COMMENT '柏悦唛头箱号;设备单箱测量时同一客户内唯一', item_no VARCHAR(32) NOT NULL COMMENT '批次内明细号', box_specification VARCHAR(64) NOT NULL COMMENT '箱规标识', package_count INT UNSIGNED NOT NULL COMMENT '相同箱规箱数', @@ -2456,18 +2457,47 @@ CREATE TABLE IF NOT EXISTS fl_inbound_measurement_item ( volume_weight_kg DECIMAL(18,3) NOT NULL COMMENT '本箱规材积重kg', calculation_rule_version VARCHAR(32) NOT NULL COMMENT '体积计算和舍入规则版本', source_image_file_id BIGINT UNSIGNED NULL COMMENT '测量设备图片文件ID', + machine_no VARCHAR(64) NULL COMMENT '本次测量设备编号', + device_measured_at DATETIME(3) NULL COMMENT '本次设备测量时间', over_limit_warning VARCHAR(500) NULL COMMENT '超线提醒快照', outer_length_cm DECIMAL(12,3) NULL COMMENT '外箱规长度cm', outer_width_cm DECIMAL(12,3) NULL COMMENT '外箱规宽度cm', outer_height_cm DECIMAL(12,3) NULL COMMENT '外箱规高度cm', remark VARCHAR(500) NULL COMMENT '测量明细备注', created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间', + updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '最后更新时间', PRIMARY KEY (id), UNIQUE KEY uk_measure_item_no (tenant_id, measurement_id, item_no), UNIQUE KEY uk_measure_external_detail (tenant_id, inbound_detail_id, external_detail_no), + UNIQUE KEY uk_measure_box_code (tenant_id, box_code), CONSTRAINT fk_measure_item_batch FOREIGN KEY (measurement_id) REFERENCES fl_inbound_measurement (id) ) ENGINE=InnoDB COMMENT='入库长宽高和箱数测量明细'; +CREATE TABLE IF NOT EXISTS fl_inbound_carton_label ( + id BIGINT UNSIGNED NOT NULL COMMENT '实际箱唛头映射ID(雪花ID)', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '当前客户隔离ID', + inbound_id BIGINT UNSIGNED NOT NULL COMMENT '入库单ID', + order_id BIGINT UNSIGNED NOT NULL COMMENT '订单服务订单ID快照', + inbound_detail_id BIGINT UNSIGNED NOT NULL COMMENT '订单货物明细ID,不建立跨库外键', + cargo_item_no INT UNSIGNED NOT NULL COMMENT '订单货物明细序号快照', + cargo_name VARCHAR(255) NULL COMMENT '货物名称快照', + detail_package_count INT UNSIGNED NOT NULL COMMENT '该订单货物明细箱数快照', + inbound_code VARCHAR(40) NOT NULL COMMENT '入仓码快照', + box_code VARCHAR(80) NOT NULL COMMENT '固定二维码内容:入仓码#总箱数-箱下标', + total_carton_count INT UNSIGNED NOT NULL COMMENT '首次分配时整单总箱数', + carton_index INT UNSIGNED NOT NULL COMMENT '整单从1开始的稳定箱下标', + box_specification VARCHAR(255) NOT NULL COMMENT '订单箱规或货物明细描述快照', + length_cm DECIMAL(12,3) NULL COMMENT '下单箱规长度cm快照', + width_cm DECIMAL(12,3) NULL COMMENT '下单箱规宽度cm快照', + height_cm DECIMAL(12,3) NULL COMMENT '下单箱规高度cm快照', + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '首次分配时间', + PRIMARY KEY (id), + UNIQUE KEY uk_carton_label_box_code (tenant_id, box_code), + UNIQUE KEY uk_carton_label_index (tenant_id, inbound_id, carton_index), + KEY idx_carton_label_detail (tenant_id, inbound_id, inbound_detail_id), + CONSTRAINT fk_carton_label_inbound FOREIGN KEY (inbound_id) REFERENCES fl_inbound_order (id) +) ENGINE=InnoDB COMMENT='首次打印前冻结的实际箱号与订单货物明细映射'; + CREATE TABLE IF NOT EXISTS fl_inbound_weight_result ( id BIGINT UNSIGNED NOT NULL COMMENT 'OCR重量结果ID', tenant_id BIGINT UNSIGNED NOT NULL COMMENT '数据所属租户ID', @@ -2601,7 +2631,7 @@ CREATE TABLE IF NOT EXISTS fl_carrier_fba_rate ( source_destination_text VARCHAR(1000) NULL COMMENT '原始组合仓库或区域文本', effective_from DATETIME(3) NOT NULL COMMENT '生效开始时间', effective_to DATETIME(3) NULL COMMENT '生效结束时间', - status VARCHAR(16) NOT NULL DEFAULT 'ACTIVE' COMMENT '价格状态', + status VARCHAR(24) NOT NULL DEFAULT 'ACTIVE' COMMENT '价格状态:PENDING_CONFIRMATION、ACTIVE或SUPERSEDED', remark VARCHAR(500) NULL COMMENT '价格备注', created_by BIGINT UNSIGNED NOT NULL COMMENT '创建人ID', created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间', @@ -2617,6 +2647,10 @@ CREATE TABLE IF NOT EXISTS fl_carrier_fba_rate ( KEY idx_fba_rate_region (price_book_version_id, channel_code, origin_warehouse_id, region_code, weight_from_kg) ) ENGINE=InnoDB COMMENT='船司FBA目的仓价格版本'; +ALTER TABLE fl_carrier_fba_rate + MODIFY COLUMN status VARCHAR(24) NOT NULL DEFAULT 'ACTIVE' + COMMENT '价格状态:PENDING_CONFIRMATION、ACTIVE或SUPERSEDED'; + CREATE TABLE IF NOT EXISTS fl_inbound_fee ( id BIGINT UNSIGNED NOT NULL COMMENT '入库费用ID', tenant_id BIGINT UNSIGNED NOT NULL COMMENT '数据所属租户ID', @@ -2871,6 +2905,8 @@ CALL phase2_measure_add_column('fl_inbound_measurement', 'version', CALL phase2_measure_add_column('fl_inbound_measurement_item', 'box_specification', 'VARCHAR(64) NOT NULL DEFAULT '''' COMMENT ''箱规标识'' AFTER item_no'); +CALL phase2_measure_add_column('fl_inbound_measurement_item', 'box_code', + 'VARCHAR(128) NULL COMMENT ''柏悦唛头箱号;设备单箱测量时同一客户内唯一'' AFTER external_detail_no'); CALL phase2_measure_add_column('fl_inbound_measurement_item', 'girth_cm', 'DECIMAL(12,3) NOT NULL DEFAULT 0 COMMENT ''周长cm,规则为长+2*(宽+高)'' AFTER height_cm'); CALL phase2_measure_add_column('fl_inbound_measurement_item', 'unit_weight_kg', @@ -2885,12 +2921,33 @@ CALL phase2_measure_add_column('fl_inbound_measurement_item', 'volume_weight_kg' 'DECIMAL(18,3) NOT NULL DEFAULT 0 COMMENT ''本箱规材积重kg'' AFTER total_volume_cbm'); CALL phase2_measure_add_column('fl_inbound_measurement_item', 'over_limit_warning', 'VARCHAR(500) NULL COMMENT ''超线提醒快照'' AFTER source_image_file_id'); +CALL phase2_measure_add_column('fl_inbound_measurement_item', 'machine_no', + 'VARCHAR(64) NULL COMMENT ''本次测量设备编号'' AFTER source_image_file_id'); +CALL phase2_measure_add_column('fl_inbound_measurement_item', 'device_measured_at', + 'DATETIME(3) NULL COMMENT ''本次设备测量时间'' AFTER machine_no'); CALL phase2_measure_add_column('fl_inbound_measurement_item', 'outer_length_cm', 'DECIMAL(12,3) NULL COMMENT ''外箱规长度cm'' AFTER over_limit_warning'); CALL phase2_measure_add_column('fl_inbound_measurement_item', 'outer_width_cm', 'DECIMAL(12,3) NULL COMMENT ''外箱规宽度cm'' AFTER outer_length_cm'); CALL phase2_measure_add_column('fl_inbound_measurement_item', 'outer_height_cm', 'DECIMAL(12,3) NULL COMMENT ''外箱规高度cm'' AFTER outer_width_cm'); +CALL phase2_measure_add_column('fl_inbound_measurement_item', 'updated_at', + 'DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT ''最后更新时间'' AFTER created_at'); + +SET @has_measure_box_code_index = ( + SELECT COUNT(*) FROM information_schema.statistics + WHERE table_schema = DATABASE() + AND table_name = 'fl_inbound_measurement_item' + AND index_name = 'uk_measure_box_code' +); +SET @phase2_measure_sql = IF( + @has_measure_box_code_index = 0, + 'ALTER TABLE fl_inbound_measurement_item ADD UNIQUE KEY uk_measure_box_code (tenant_id, box_code)', + 'SELECT 1' +); +PREPARE phase2_measure_statement FROM @phase2_measure_sql; +EXECUTE phase2_measure_statement; +DEALLOCATE PREPARE phase2_measure_statement; DROP PROCEDURE IF EXISTS phase2_measure_add_column; @@ -5212,6 +5269,30 @@ BEGIN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '统一迁移失败:客户费用规则缺少客户 tenant_id'; END IF; + IF (SELECT COUNT(*) FROM information_schema.tables + WHERE table_schema = 'baiyue_firstleg' + AND table_name = 'fl_inbound_carton_label') <> 1 THEN + SIGNAL SQLSTATE '45000' + SET MESSAGE_TEXT = '统一迁移失败:稳定箱号映射表未创建'; + END IF; + IF (SELECT COUNT(*) FROM information_schema.columns + WHERE table_schema = 'baiyue_firstleg' + AND table_name = 'fl_inbound_measurement_item' + AND column_name IN ('box_code', 'machine_no', 'device_measured_at', 'updated_at')) <> 4 THEN + SIGNAL SQLSTATE '45000' + SET MESSAGE_TEXT = '统一迁移失败:单箱当前测量字段未完整创建'; + END IF; + IF (SELECT COUNT(DISTINCT CONCAT(table_name, '.', index_name)) + FROM information_schema.statistics + WHERE table_schema = 'baiyue_firstleg' + AND ((table_name = 'fl_inbound_carton_label' + AND index_name IN ('uk_carton_label_box_code', 'uk_carton_label_index')) + OR (table_name = 'fl_inbound_measurement_item' + AND index_name = 'uk_measure_box_code')) + AND non_unique = 0) <> 3 THEN + SIGNAL SQLSTATE '45000' + SET MESSAGE_TEXT = '统一迁移失败:稳定箱号或单箱测量唯一约束未完整创建'; + END IF; END$$ DELIMITER ; CALL unified_assert_final_extensions(); @@ -5223,4 +5304,4 @@ SELECT COUNT(*) AS unresolved_third_party_calls WHERE tenant_id = 0; SELECT 'PASS' AS migration_status, - '统一增量全部执行完成;统一数据库脚本执行完成,三期出仓、费用、公共资源及可靠审计结构校验通过' AS message; + '统一增量全部执行完成;箱号映射、单箱测量、三期出仓、费用、公共资源及可靠审计结构校验通过' AS message; diff --git a/firstleg/src/main/java/com/baiyue/firstleg/service/PlatformPriceBookService.java b/firstleg/src/main/java/com/baiyue/firstleg/service/PlatformPriceBookService.java index f1f53adf3..0b9a768c1 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/service/PlatformPriceBookService.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/service/PlatformPriceBookService.java @@ -395,8 +395,12 @@ public class PlatformPriceBookService { issues.add(issue("BLOCKING", "SOURCE_LOCATION_MISSING", "价格行缺少Excel来源位置", rate.sourceSheetName(), rate.sourceRowNumber(), null)); validateOrigin(rate, warehouseCache, issues); validateDestination(rate, warehouseCache, issues); - String key = String.join("|", code, text(rate.originWarehouseId()), enumText(rate.destinationType()), - text(rate.destinationWarehouseId()), nullToEmpty(upper(rate.destinationCode())), + String originIdentity = rate.originWarehouseId() == null + ? "CODE:" + mappingKey(rate.originWarehouseCode()) : "ID:" + rate.originWarehouseId(); + String destinationIdentity = rate.destinationWarehouseId() == null + ? "NAME:" + mappingKey(rate.destinationName()) : "ID:" + rate.destinationWarehouseId(); + String key = String.join("|", code, originIdentity, enumText(rate.destinationType()), + destinationIdentity, nullToEmpty(upper(rate.destinationCode())), rate.regionCode() == null ? "" : rate.regionCode().name(), rate.pricingTaxMode() == null ? "" : rate.pricingTaxMode().name(), rate.chargeUnit(), text(rate.weightFromKg())); if (!unique.add(key)) issues.add(issue("BLOCKING", "DUPLICATE_RATE", "存在重复价格组合", rate.sourceSheetName(), rate.sourceRowNumber(), null)); diff --git a/firstleg/src/main/resources/db/phase2-inbound-schema.sql b/firstleg/src/main/resources/db/phase2-inbound-schema.sql index 011d8bef9..a72ee5706 100644 --- a/firstleg/src/main/resources/db/phase2-inbound-schema.sql +++ b/firstleg/src/main/resources/db/phase2-inbound-schema.sql @@ -368,7 +368,7 @@ CREATE TABLE IF NOT EXISTS fl_carrier_fba_rate ( source_destination_text VARCHAR(1000) NULL COMMENT '原始组合仓库或区域文本', effective_from DATETIME(3) NOT NULL COMMENT '生效开始时间', effective_to DATETIME(3) NULL COMMENT '生效结束时间', - status VARCHAR(16) NOT NULL DEFAULT 'ACTIVE' COMMENT '价格状态', + status VARCHAR(24) NOT NULL DEFAULT 'ACTIVE' COMMENT '价格状态:PENDING_CONFIRMATION、ACTIVE或SUPERSEDED', remark VARCHAR(500) NULL COMMENT '价格备注', created_by BIGINT UNSIGNED NOT NULL COMMENT '创建人ID', created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间', diff --git a/firstleg/src/main/resources/openapi/firstleg-v1.yaml b/firstleg/src/main/resources/openapi/firstleg-v1.yaml index 4460ba26f..25bf76235 100644 --- a/firstleg/src/main/resources/openapi/firstleg-v1.yaml +++ b/firstleg/src/main/resources/openapi/firstleg-v1.yaml @@ -163,6 +163,35 @@ paths: responses: '200': {description: 成功或业务失败,按响应体 code 判断} '403': {description: 无查看权限} + /api/platform/inbounds/{inboundId}/carton-label-pdf: + post: + summary: 生成或补打入仓箱唛 PDF + description: 需要 platform:firstleg:inbound:view 权限;首次打印冻结整单箱号,重复打印和部分补打复用原映射。 + parameters: + - {name: tenantId, in: query, required: true, schema: {type: string, pattern: '^[1-9][0-9]*$'}} + - {name: inboundId, in: path, required: true, schema: {type: string, pattern: '^[1-9][0-9]*$'}} + - name: cartonIndex + in: query + required: false + style: form + explode: true + schema: {type: array, items: {type: integer, minimum: 1}} + description: 不传时打印整单;可重复传入以部分补打指定箱下标。 + responses: + '200': + description: 成功返回 PDF;业务失败返回统一 JSON 响应,调用方必须按 Content-Type 区分。 + content: + application/pdf: {schema: {type: string, format: binary}} + application/json: + schema: + type: object + required: [code, message] + properties: + code: {type: string} + message: {type: string} + requestId: {type: string, nullable: true} + traceId: {type: string, nullable: true} + '403': {description: 无查看权限} /api/platform/v1/inbounds/{inboundId}/measurements/approve: post: summary: 审核通过测量 diff --git a/firstleg/src/test/java/com/baiyue/firstleg/controller/InboundOrderControllerContractTest.java b/firstleg/src/test/java/com/baiyue/firstleg/controller/InboundOrderControllerContractTest.java index fe010dca1..40fb20538 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/controller/InboundOrderControllerContractTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/controller/InboundOrderControllerContractTest.java @@ -44,6 +44,7 @@ class InboundOrderControllerContractTest { assertThat(openapi) .contains("/api/platform/inbounds:") .contains("/api/platform/inbounds/{inboundId}:") + .contains("/api/platform/inbounds/{inboundId}/carton-label-pdf:") .contains("required: false") .contains("InboundOrderSummary:") .contains("required: [id, tenantId]") diff --git a/firstleg/src/test/java/com/baiyue/firstleg/repository/InboundCartonLabelSqlContractTest.java b/firstleg/src/test/java/com/baiyue/firstleg/repository/InboundCartonLabelSqlContractTest.java index dd4165f58..9452d8463 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/repository/InboundCartonLabelSqlContractTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/repository/InboundCartonLabelSqlContractTest.java @@ -4,6 +4,8 @@ import org.junit.jupiter.api.Test; import org.springframework.core.io.ClassPathResource; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; import static org.assertj.core.api.Assertions.assertThat; @@ -12,6 +14,7 @@ class InboundCartonLabelSqlContractTest { void baseSchemaAndExistingDatabaseMigrationKeepStableCartonKeys() throws Exception { String schema = resource("db/phase2-inbound-schema.sql"); String migration = resource("db/phase2-inbound-carton-label.sql"); + String unified = Files.readString(Path.of("..", "deploy", "sql", "头程数据库统一增量-MySQL8.sql")); assertThat(schema).contains("CREATE TABLE IF NOT EXISTS fl_inbound_carton_label") .contains("UNIQUE KEY uk_carton_label_box_code (tenant_id, box_code)") @@ -20,6 +23,10 @@ class InboundCartonLabelSqlContractTest { assertThat(migration).contains("CREATE TABLE IF NOT EXISTS fl_inbound_carton_label") .contains("固定二维码内容:入仓码#总箱数-箱下标") .contains("idx_carton_label_detail (tenant_id, inbound_id, inbound_detail_id)"); + assertThat(unified).contains("CREATE TABLE IF NOT EXISTS fl_inbound_carton_label") + .contains("uk_carton_label_box_code") + .contains("uk_carton_label_index") + .contains("稳定箱号映射表未创建"); } private String resource(String path) throws Exception { diff --git a/firstleg/src/test/java/com/baiyue/firstleg/repository/InboundWorkflowSqlContractTest.java b/firstleg/src/test/java/com/baiyue/firstleg/repository/InboundWorkflowSqlContractTest.java index c5bcf4e41..637251088 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/repository/InboundWorkflowSqlContractTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/repository/InboundWorkflowSqlContractTest.java @@ -5,6 +5,8 @@ import org.junit.jupiter.api.Test; import org.springframework.core.io.ClassPathResource; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; import static org.assertj.core.api.Assertions.assertThat; @@ -46,6 +48,7 @@ class InboundWorkflowSqlContractTest { .getContentAsString(StandardCharsets.UTF_8); String migration = new ClassPathResource("db/phase2-carton-measurement-current-record.sql") .getContentAsString(StandardCharsets.UTF_8); + String unified = Files.readString(Path.of("..", "deploy", "sql", "头程数据库统一增量-MySQL8.sql")); assertThat(schema) .contains("box_code VARCHAR(128)") .contains("machine_no VARCHAR(64)") @@ -55,5 +58,9 @@ class InboundWorkflowSqlContractTest { .contains("phase2_add_carton_measurement_column") .contains("uk_measure_box_code") .contains("information_schema.STATISTICS"); + assertThat(unified) + .contains("CALL phase2_measure_add_column('fl_inbound_measurement_item', 'box_code'") + .contains("uk_measure_box_code") + .contains("单箱当前测量字段未完整创建"); } } diff --git a/firstleg/src/test/java/com/baiyue/firstleg/repository/PriceBookSqlContractTest.java b/firstleg/src/test/java/com/baiyue/firstleg/repository/PriceBookSqlContractTest.java index 340264867..e7958817d 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/repository/PriceBookSqlContractTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/repository/PriceBookSqlContractTest.java @@ -21,6 +21,10 @@ class PriceBookSqlContractTest { schema.indexOf("CREATE TABLE IF NOT EXISTS fl_price_book_channel"), schema.indexOf("ENGINE=InnoDB COMMENT='价格版本渠道及船期快照'") ); + String rateTable = schema.substring( + schema.indexOf("CREATE TABLE IF NOT EXISTS fl_carrier_fba_rate"), + schema.indexOf("ENGINE=InnoDB COMMENT='船司FBA目的仓价格版本'") + ); assertThat(schema) .contains("CREATE TABLE IF NOT EXISTS fl_price_channel_catalog") .contains("CREATE TABLE IF NOT EXISTS fl_price_origin_alias") @@ -41,6 +45,9 @@ class PriceBookSqlContractTest { .contains("updated_by BIGINT UNSIGNED NOT NULL COMMENT '最后修改人ID'") .contains("deleted TINYINT(1) NOT NULL DEFAULT 0 COMMENT '逻辑删除标记'") .contains("version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号'"); + assertThat(rateTable) + .contains("status VARCHAR(24) NOT NULL DEFAULT 'ACTIVE'") + .contains("PENDING_CONFIRMATION、ACTIVE或SUPERSEDED"); assertThat(migration) .doesNotContain("fl_price_book_version") .doesNotContain("'pricing_tax_mode'") diff --git a/firstleg/src/test/java/com/baiyue/firstleg/service/PlatformPriceBookServiceTest.java b/firstleg/src/test/java/com/baiyue/firstleg/service/PlatformPriceBookServiceTest.java index ef24dc45f..2e4d70189 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/service/PlatformPriceBookServiceTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/service/PlatformPriceBookServiceTest.java @@ -205,6 +205,31 @@ class PlatformPriceBookServiceTest { verify(repository, never()).deletePendingChildren(22L); } + @Test + void previewDoesNotTreatDifferentUnmappedOriginsAsDuplicateRates() { + CarrierFbaRateEntity yiwu = rate(79L); + CarrierFbaRateEntity dongguan = rate(80L); + dongguan.setOriginWarehouseCode("CN-DONGGUAN-01"); + dongguan.setOriginWarehouseName("东莞仓"); + when(repository.activeCatalog()).thenReturn(List.of(catalog("MATSON_CLX_TRUCK"))); + PriceBookDtos.ParsedChannel parsedChannel = ReflectionTestUtils.invokeMethod( + service, "channelDto", channel("MATSON_CLX_TRUCK")); + PriceBookDtos.ParsedRate parsedYiwu = ReflectionTestUtils.invokeMethod(service, "rateDto", yiwu); + PriceBookDtos.ParsedRate parsedDongguan = ReflectionTestUtils.invokeMethod(service, "rateDto", dongguan); + + @SuppressWarnings("unchecked") + List issues = ReflectionTestUtils.invokeMethod( + service, + "validate", + List.of(parsedChannel), + List.of(parsedYiwu, parsedDongguan) + ); + + assertThat(issues).extracting(PriceBookDtos.ParseIssue::issueCode) + .contains("ORIGIN_MAPPING_REQUIRED") + .doesNotContain("DUPLICATE_RATE"); + } + @Test void duplicatePublishIsRejectedAfterPublishLock() { when(repository.findVersion(31L)).thenReturn(version(31L, PriceBookStatus.ACTIVE));