From 3f00896411ba35010ad9e2b613187edfb444561b Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Tue, 29 Sep 2026 04:21:46 +0100 Subject: [PATCH] test(firstleg): active price book sailing channels --- .../V4__cargo_discard_return_inventory.sql | 7 -- deploy/sql/海外区域统一增量-MySQL8.sql | 4 +- .../sql/海外货物废弃出库字段补齐-MySQL8.sql | 75 --------------- .../out/persistence/PriceBookQueryMapper.java | 22 ++++- .../application/PriceBookQueryService.java | 1 + .../application/model/PriceBookQueryRows.java | 1 + .../port/in/PriceBookQueryUseCase.java | 1 + .../port/out/PriceBookQueryReader.java | 1 + .../dto/SailingCalendarDtos.java | 2 +- .../service/SailingCalendarService.java | 77 ++++++++++----- .../controller/CargoDiscardController.java | 4 +- .../discard/dto/CargoDiscardResponses.java | 4 +- .../entity/CargoDiscardApplication.java | 2 - .../discard/service/CargoDiscardService.java | 96 +------------------ .../lastleg/relabel/dto/RelabelRequests.java | 5 +- .../lastleg/relabel/dto/RelabelResponses.java | 3 +- .../service/RelabelApplicationService.java | 26 +---- .../MerchantReturnOrderController.java | 5 - .../dto/WarehouseTransferRequests.java | 5 +- .../dto/WarehouseTransferResponses.java | 4 +- .../entity/WarehouseTransferOrder.java | 2 - .../mapper/WarehouseTransferOrderMapper.java | 4 +- .../service/WarehouseTransferService.java | 49 +++++----- .../src/main/resources/db/lastleg.sql | 8 +- .../openapi/overseas-business-v1.yaml | 7 -- ...rchantOverseasCustomerApiContractTest.java | 1 - ...chantRelabelApplicationControllerTest.java | 3 +- ...rchantWarehouseTransferControllerTest.java | 3 +- .../service/WarehouseTransferServiceTest.java | 68 ++----------- scripts/db/incremental.sql | 10 +- 30 files changed, 135 insertions(+), 365 deletions(-) delete mode 100644 deploy/sql/海外货物废弃出库字段补齐-MySQL8.sql diff --git a/deploy/sql/migrations/overseas/baiyue_lastleg/V4__cargo_discard_return_inventory.sql b/deploy/sql/migrations/overseas/baiyue_lastleg/V4__cargo_discard_return_inventory.sql index 308970df7..ff1784f50 100644 --- a/deploy/sql/migrations/overseas/baiyue_lastleg/V4__cargo_discard_return_inventory.sql +++ b/deploy/sql/migrations/overseas/baiyue_lastleg/V4__cargo_discard_return_inventory.sql @@ -3,10 +3,6 @@ ALTER TABLE ll_cargo_discard_item MODIFY COLUMN profile_id BIGINT NULL COMMENT '托盘档案ID', MODIFY COLUMN sku_inventory_id BIGINT NULL COMMENT 'SKU库存记录ID'; -SET @ddl = IF(NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_cargo_discard_application' AND column_name='completed_at'), - 'ALTER TABLE ll_cargo_discard_application ADD COLUMN completed_at DATETIME(3) NULL COMMENT ''废弃完成时间'' AFTER reviewed_at', 'SELECT 1'); -PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt; - SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.table_constraints WHERE constraint_schema=DATABASE() AND table_name='ll_return_inventory_carton' AND constraint_name='chk_ll_return_inventory_carton_quantity'), 'ALTER TABLE ll_return_inventory_carton DROP CHECK chk_ll_return_inventory_carton_quantity', 'SELECT 1')); PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt; ALTER TABLE ll_return_inventory_carton ADD CONSTRAINT chk_ll_return_inventory_carton_quantity CHECK((status IN('AVAILABLE','RELABEL_PENDING','OUTBOUNDED','DISCARD_PENDING') AND sku_types>0 AND total_quantity>0) OR (status IN('MERGED','DISCARDED') AND sku_types=0 AND total_quantity=0)); @@ -26,6 +22,3 @@ PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt; SET @ddl = IF(NOT EXISTS (SELECT 1 FROM information_schema.statistics WHERE table_schema=DATABASE() AND table_name='ll_cargo_discard_item' AND index_name='idx_ll_cargo_discard_return_carton'), 'ALTER TABLE ll_cargo_discard_item ADD KEY idx_ll_cargo_discard_return_carton(customer_id,return_carton_id)', 'SELECT 1'); PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt; -SET @ddl = IF(NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_cargo_discard_application' AND column_name='outbound_no'), - 'ALTER TABLE ll_cargo_discard_application ADD COLUMN outbound_no VARCHAR(64) NULL COMMENT ''废弃出库单号'' AFTER discard_no', 'SELECT 1'); -PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt; diff --git a/deploy/sql/海外区域统一增量-MySQL8.sql b/deploy/sql/海外区域统一增量-MySQL8.sql index 4c89ab0b5..176fe0178 100644 --- a/deploy/sql/海外区域统一增量-MySQL8.sql +++ b/deploy/sql/海外区域统一增量-MySQL8.sql @@ -95,7 +95,7 @@ USE baiyue_lastleg; CREATE TABLE ll_cargo_discard_application ( id bigint NOT NULL COMMENT '主键ID', customer_id varchar(128) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '客户租户ID', - discard_no varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '废弃申请单号', outbound_no varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '废弃出库单号', + discard_no varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '废弃申请单号', overseas_warehouse_id varchar(128) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '海外仓ID', overseas_warehouse_name varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '海外仓名称快照', inbound_codes varchar(2000) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '入仓单号集合', @@ -112,7 +112,7 @@ CREATE TABLE ll_cargo_discard_application ( review_remark varchar(1000) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '审核备注', review_idempotency_key varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '审核幂等键', review_request_hash char(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '审核请求内容哈希', - reviewed_at datetime(3) DEFAULT NULL COMMENT '审核时间', completed_at datetime(3) DEFAULT NULL COMMENT '废弃完成时间', + reviewed_at datetime(3) DEFAULT 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), diff --git a/deploy/sql/海外货物废弃出库字段补齐-MySQL8.sql b/deploy/sql/海外货物废弃出库字段补齐-MySQL8.sql deleted file mode 100644 index ab43063ab..000000000 --- a/deploy/sql/海外货物废弃出库字段补齐-MySQL8.sql +++ /dev/null @@ -1,75 +0,0 @@ --- 货物废弃申请 + 废弃出库单字段补齐 --- 目标数据库:baiyue_lastleg --- MySQL 8.0;脚本可重复执行,已存在的字段/索引会跳过。 -USE baiyue_lastleg; - -SET @ddl = IF(NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_outbound_pallet_item' AND column_name='warehouse_pallet_id'), - 'ALTER TABLE ll_outbound_pallet_item ADD COLUMN warehouse_pallet_id BIGINT NULL COMMENT ''仓库实物托盘ID'' AFTER outbound_order_id', - IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_outbound_pallet_item' AND column_name='warehouse_pallet_id' AND (is_nullable <> 'YES' OR COALESCE(column_comment,'') <> '仓库实物托盘ID')), - 'ALTER TABLE ll_outbound_pallet_item MODIFY COLUMN warehouse_pallet_id BIGINT NULL COMMENT ''仓库实物托盘ID''', 'SELECT 1')); -PREPARE outbound_pallet_schema FROM @ddl; EXECUTE outbound_pallet_schema; DEALLOCATE PREPARE outbound_pallet_schema; - - -SET @ddl = IF(NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_cargo_discard_application' AND column_name='outbound_no'), - 'ALTER TABLE ll_cargo_discard_application ADD COLUMN outbound_no VARCHAR(64) NULL COMMENT ''废弃出库单号'' AFTER discard_no', 'SELECT 1'); -PREPARE s FROM @ddl; EXECUTE s; DEALLOCATE PREPARE s; -SET @ddl = IF(NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_cargo_discard_application' AND column_name='completed_at'), - 'ALTER TABLE ll_cargo_discard_application ADD COLUMN completed_at DATETIME(3) NULL COMMENT ''废弃完成时间'' AFTER reviewed_at', 'SELECT 1'); -PREPARE s FROM @ddl; EXECUTE s; DEALLOCATE PREPARE s; - -SET @ddl = IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_cargo_discard_item' AND column_name='order_id' AND is_nullable='NO'), - 'ALTER TABLE ll_cargo_discard_item MODIFY COLUMN order_id BIGINT NULL COMMENT ''一件代发订单ID''', 'SELECT 1'); -PREPARE s FROM @ddl; EXECUTE s; DEALLOCATE PREPARE s; -SET @ddl = IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_cargo_discard_item' AND column_name='profile_id' AND is_nullable='NO'), - 'ALTER TABLE ll_cargo_discard_item MODIFY COLUMN profile_id BIGINT NULL COMMENT ''托盘档案ID''', 'SELECT 1'); -PREPARE s FROM @ddl; EXECUTE s; DEALLOCATE PREPARE s; -SET @ddl = IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_cargo_discard_item' AND column_name='sku_inventory_id' AND is_nullable='NO'), - 'ALTER TABLE ll_cargo_discard_item MODIFY COLUMN sku_inventory_id BIGINT NULL COMMENT ''SKU库存记录ID''', 'SELECT 1'); -PREPARE s FROM @ddl; EXECUTE s; DEALLOCATE PREPARE s; - -SET @ddl = IF(NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_cargo_discard_item' AND column_name='source_type'), - 'ALTER TABLE ll_cargo_discard_item ADD COLUMN source_type VARCHAR(32) NOT NULL DEFAULT ''DROPSHIP'' COMMENT ''库存来源:DROPSHIP/RETURN_CARTON'' AFTER order_id', 'SELECT 1'); -PREPARE s FROM @ddl; EXECUTE s; DEALLOCATE PREPARE s; -SET @ddl = IF(NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_cargo_discard_item' AND column_name='return_carton_id'), - 'ALTER TABLE ll_cargo_discard_item ADD COLUMN return_carton_id BIGINT NULL COMMENT ''退货库存箱ID'' AFTER source_type', 'SELECT 1'); -PREPARE s FROM @ddl; EXECUTE s; DEALLOCATE PREPARE s; -SET @ddl = IF(NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_cargo_discard_item' AND column_name='actual_discard_packages'), - 'ALTER TABLE ll_cargo_discard_item ADD COLUMN actual_discard_packages INT NULL COMMENT ''PDA确认实际废弃箱数'' AFTER discard_packages', 'SELECT 1'); -PREPARE s FROM @ddl; EXECUTE s; DEALLOCATE PREPARE s; -SET @ddl = IF(NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_cargo_discard_item' AND column_name='pda_status'), - 'ALTER TABLE ll_cargo_discard_item ADD COLUMN pda_status VARCHAR(32) NOT NULL DEFAULT ''PENDING'' COMMENT ''PDA状态:PENDING/COMPLETED'' AFTER actual_discard_packages', 'SELECT 1'); -PREPARE s FROM @ddl; EXECUTE s; DEALLOCATE PREPARE s; -SET @ddl = IF(NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_cargo_discard_item' AND column_name='pda_operator_id'), - 'ALTER TABLE ll_cargo_discard_item ADD COLUMN pda_operator_id VARCHAR(128) NULL COMMENT ''PDA操作人'' AFTER pda_status', 'SELECT 1'); -PREPARE s FROM @ddl; EXECUTE s; DEALLOCATE PREPARE s; -SET @ddl = IF(NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_cargo_discard_item' AND column_name='pda_completed_at'), - 'ALTER TABLE ll_cargo_discard_item ADD COLUMN pda_completed_at DATETIME(3) NULL COMMENT ''PDA完成时间'' AFTER pda_operator_id', 'SELECT 1'); -PREPARE s FROM @ddl; EXECUTE s; DEALLOCATE PREPARE s; -SET @ddl = IF(NOT EXISTS (SELECT 1 FROM information_schema.statistics WHERE table_schema=DATABASE() AND table_name='ll_cargo_discard_item' AND index_name='idx_ll_cargo_discard_return_carton'), - 'ALTER TABLE ll_cargo_discard_item ADD KEY idx_ll_cargo_discard_return_carton(customer_id,return_carton_id)', 'SELECT 1'); -PREPARE s FROM @ddl; EXECUTE s; DEALLOCATE PREPARE s; - -SET @ddl = IF(NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_outbound_order' AND column_name='outbound_type'), - 'ALTER TABLE ll_outbound_order ADD COLUMN outbound_type VARCHAR(32) NOT NULL DEFAULT ''DISPATCH_DELIVERY'' COMMENT ''出库类型'' AFTER status', 'SELECT 1'); -PREPARE s FROM @ddl; EXECUTE s; DEALLOCATE PREPARE s; -SET @ddl = IF(NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_outbound_order' AND column_name='recipient_info'), - 'ALTER TABLE ll_outbound_order ADD COLUMN recipient_info VARCHAR(1000) NULL COMMENT ''收件/业务说明'' AFTER lx_codes', 'SELECT 1'); -PREPARE s FROM @ddl; EXECUTE s; DEALLOCATE PREPARE s; -SET @ddl = IF(NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_outbound_order' AND column_name='request_hash'), - 'ALTER TABLE ll_outbound_order ADD COLUMN request_hash CHAR(64) NULL COMMENT ''请求摘要'' AFTER recipient_info', 'SELECT 1'); -PREPARE s FROM @ddl; EXECUTE s; DEALLOCATE PREPARE s; - -SET @ddl = IF(NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_outbound_pallet_item' AND column_name='sku_code'), - 'ALTER TABLE ll_outbound_pallet_item ADD COLUMN sku_code VARCHAR(128) NULL COMMENT ''SKU''', 'SELECT 1'); -PREPARE s FROM @ddl; EXECUTE s; DEALLOCATE PREPARE s; -SET @ddl = IF(NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_outbound_pallet_item' AND column_name='carton_marks'), - 'ALTER TABLE ll_outbound_pallet_item ADD COLUMN carton_marks VARCHAR(4000) NULL COMMENT ''箱唛快照'' AFTER sku_code', 'SELECT 1'); -PREPARE s FROM @ddl; EXECUTE s; DEALLOCATE PREPARE s; - --- 退货库存废弃需要允许中间锁定状态;若本地已有约束,按名称重建。 -SET @ddl = IF(EXISTS (SELECT 1 FROM information_schema.table_constraints WHERE constraint_schema=DATABASE() AND table_name='ll_return_inventory_carton' AND constraint_name='chk_ll_return_inventory_carton_status'), - 'ALTER TABLE ll_return_inventory_carton DROP CHECK chk_ll_return_inventory_carton_status', 'SELECT 1'); -PREPARE s FROM @ddl; EXECUTE s; DEALLOCATE PREPARE s; -ALTER TABLE ll_return_inventory_carton ADD CONSTRAINT chk_ll_return_inventory_carton_status CHECK(status IN('AVAILABLE','RELABEL_PENDING','MERGED','OUTBOUNDED','DISCARD_PENDING','DISCARDED')); - -SELECT 'cargo discard/outbound schema patch completed' AS result, DATABASE() AS database_name; diff --git a/firstleg/src/main/java/com/baiyue/firstleg/pricebookquery/adapter/out/persistence/PriceBookQueryMapper.java b/firstleg/src/main/java/com/baiyue/firstleg/pricebookquery/adapter/out/persistence/PriceBookQueryMapper.java index 7c01a22c5..dbf759f82 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/pricebookquery/adapter/out/persistence/PriceBookQueryMapper.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/pricebookquery/adapter/out/persistence/PriceBookQueryMapper.java @@ -43,7 +43,7 @@ public interface PriceBookQueryMapper extends PriceBookQueryReader { Long findEffectiveVersionId(@Param("customerId") long customerId); @Select(""" - SELECT c.price_book_version_id AS priceBookVersionId, + SELECT catalog.id AS channelCatalogId, c.price_book_version_id AS priceBookVersionId, c.channel_code AS channelCode, c.channel_name AS channelName, c.price_model AS priceModel, c.estimated_departure_date AS estimatedDepartureDate, @@ -53,6 +53,8 @@ public interface PriceBookQueryMapper extends PriceBookQueryReader { c.surcharge_description AS surchargeDescription, c.remark AS remark FROM fl_price_book_channel c + JOIN fl_price_channel_catalog catalog + ON catalog.channel_code = c.channel_code AND catalog.status = 'ACTIVE' WHERE c.price_book_version_id = #{versionId} AND EXISTS ( SELECT 1 FROM fl_carrier_fba_rate r @@ -66,6 +68,24 @@ public interface PriceBookQueryMapper extends PriceBookQueryReader { List findChannels(@Param("versionId") long versionId, @Param("originWarehouseId") long originWarehouseId); + @Select(""" + SELECT catalog.id AS channelCatalogId, c.price_book_version_id AS priceBookVersionId, + c.channel_code AS channelCode, c.channel_name AS channelName, + c.price_model AS priceModel, + c.estimated_departure_date AS estimatedDepartureDate, + c.estimated_departure_text AS estimatedDepartureText, + c.latest_warehouse_entry AS latestWarehouseEntry, + c.reference_transit AS referenceTransit, + c.surcharge_description AS surchargeDescription, + c.remark AS remark + FROM fl_price_book_channel c + JOIN fl_price_channel_catalog catalog + ON catalog.channel_code = c.channel_code AND catalog.status = 'ACTIVE' + WHERE c.price_book_version_id = #{versionId} + ORDER BY c.id + """) + List findAllChannels(@Param("versionId") long versionId); + @Select(""" SELECT r.channel_code AS channelCode, r.pricing_tax_mode AS pricingTaxMode FROM fl_carrier_fba_rate r diff --git a/firstleg/src/main/java/com/baiyue/firstleg/pricebookquery/application/PriceBookQueryService.java b/firstleg/src/main/java/com/baiyue/firstleg/pricebookquery/application/PriceBookQueryService.java index e3f07440e..48eceeaaa 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/pricebookquery/application/PriceBookQueryService.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/pricebookquery/application/PriceBookQueryService.java @@ -22,6 +22,7 @@ public class PriceBookQueryService implements PriceBookQueryUseCase { @Override public List channels(long versionId, long originWarehouseId) { return reader.findChannels(versionId, originWarehouseId); } + @Override public List allChannels(long versionId) { return reader.findAllChannels(versionId); } @Override public List channelTaxModes(long versionId, long originWarehouseId) { return reader.findChannelTaxModes(versionId, originWarehouseId); } diff --git a/firstleg/src/main/java/com/baiyue/firstleg/pricebookquery/application/model/PriceBookQueryRows.java b/firstleg/src/main/java/com/baiyue/firstleg/pricebookquery/application/model/PriceBookQueryRows.java index 0ca9c0b8a..e969cf285 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/pricebookquery/application/model/PriceBookQueryRows.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/pricebookquery/application/model/PriceBookQueryRows.java @@ -16,6 +16,7 @@ public final class PriceBookQueryRows { @Getter @Setter public static class ChannelRow { + private Long channelCatalogId; private Long priceBookVersionId; private String channelCode; private String channelName; diff --git a/firstleg/src/main/java/com/baiyue/firstleg/pricebookquery/application/port/in/PriceBookQueryUseCase.java b/firstleg/src/main/java/com/baiyue/firstleg/pricebookquery/application/port/in/PriceBookQueryUseCase.java index 681279a2e..14a3e2d75 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/pricebookquery/application/port/in/PriceBookQueryUseCase.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/pricebookquery/application/port/in/PriceBookQueryUseCase.java @@ -11,6 +11,7 @@ public interface PriceBookQueryUseCase { Long activeVersionId(); Long effectiveVersionId(long customerId); List channels(long versionId, long originWarehouseId); + List allChannels(long versionId); List channelTaxModes(long versionId, long originWarehouseId); List fbaRates(long versionId, long originWarehouseId, String channelCode, String destinationCode, String pricingTaxMode); diff --git a/firstleg/src/main/java/com/baiyue/firstleg/pricebookquery/application/port/out/PriceBookQueryReader.java b/firstleg/src/main/java/com/baiyue/firstleg/pricebookquery/application/port/out/PriceBookQueryReader.java index d12366820..f441f1d68 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/pricebookquery/application/port/out/PriceBookQueryReader.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/pricebookquery/application/port/out/PriceBookQueryReader.java @@ -11,6 +11,7 @@ public interface PriceBookQueryReader { Long findActiveVersionId(); Long findEffectiveVersionId(long customerId); List findChannels(long versionId, long originWarehouseId); + List findAllChannels(long versionId); List findChannelTaxModes(long versionId, long originWarehouseId); List findFbaRates(long versionId, long originWarehouseId, String channelCode, String destinationCode, String pricingTaxMode); diff --git a/firstleg/src/main/java/com/baiyue/firstleg/sailingcalendar/dto/SailingCalendarDtos.java b/firstleg/src/main/java/com/baiyue/firstleg/sailingcalendar/dto/SailingCalendarDtos.java index 4f44ad03d..e626f65ac 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/sailingcalendar/dto/SailingCalendarDtos.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/sailingcalendar/dto/SailingCalendarDtos.java @@ -19,7 +19,7 @@ import java.util.List; public final class SailingCalendarDtos { private SailingCalendarDtos() {} - public record ChannelQuery(@Size(max = 64) String keyword, String status) {} + public record ChannelQuery(@Size(max = 64) String keyword, String status, @Positive Long warehouseId) {} public record ChannelData(@NotBlank @Size(max = 64) String channelCode, @NotBlank @Size(max = 128) String channelName, @NotBlank String sourceType, diff --git a/firstleg/src/main/java/com/baiyue/firstleg/sailingcalendar/service/SailingCalendarService.java b/firstleg/src/main/java/com/baiyue/firstleg/sailingcalendar/service/SailingCalendarService.java index 5b3450f46..425510c4f 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/sailingcalendar/service/SailingCalendarService.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/sailingcalendar/service/SailingCalendarService.java @@ -29,6 +29,8 @@ import com.baiyue.firstleg.sailingcalendar.fact.entity.SailingAssignmentEntity; import com.baiyue.firstleg.sailingcalendar.fact.entity.SailingBookingLinkEntity; import com.baiyue.firstleg.sailingcalendar.fact.entity.SailingInboundVolumeForecastEntity; import com.baiyue.firstleg.sailingcalendar.fact.entity.SailingReceiptBatchFactEntity; +import com.baiyue.firstleg.pricebookquery.application.model.PriceBookQueryRows.ChannelRow; +import com.baiyue.firstleg.pricebookquery.application.port.in.PriceBookQueryUseCase; import com.baiyue.firstleg.sailingcalendar.mapper.SailingCalendarDisplayMapper; import com.baiyue.firstleg.oceanbooking.entity.OceanBookingContainerRequirementEntity; import com.baiyue.firstleg.oceanbooking.entity.OceanBookingEntity; @@ -41,30 +43,30 @@ public class SailingCalendarService { private static final ZoneId ZONE = ZoneId.of("Asia/Shanghai"); private static final String OPEN = "OPEN"; @Resource private SailingCalendarRepository repository; + @Resource private PriceBookQueryUseCase priceBooks; public List channels(ChannelQuery query) { - return repository.channels(query).stream().map(v -> new ChannelView(v, "ACTIVE".equals(v.getStatus()))).toList(); + Long versionId = priceBooks.activeVersionId(); + if (versionId == null || "INACTIVE".equalsIgnoreCase(trim(query.status()))) return List.of(); + List rows = query.warehouseId() == null + ? priceBooks.allChannels(versionId) + : priceBooks.channels(versionId, query.warehouseId()); + String keyword = trimUpper(query.keyword()); + return rows.stream() + .filter(row -> !StringUtils.hasText(keyword) + || (row.getChannelCode() != null && row.getChannelCode().toUpperCase().contains(keyword)) + || (row.getChannelName() != null && row.getChannelName().toUpperCase().contains(keyword))) + .map(this::priceBookChannel) + .map(v -> new ChannelView(v, false)) + .toList(); } @Transactional public SailingChannelEntity createChannel(long operatorId, String operatorName, ChannelCreateRequest request) { - if (version(request.expectedVersion()) != 0) throw bad("新建渠道expectedVersion必须为0"); - validateChannel(request.data()); - if (repository.channelByCode(request.data().channelCode().trim().toUpperCase()) != null) throw conflict("渠道代码已存在"); - SailingChannelEntity value = new SailingChannelEntity(); value.setId(IdWorker.getId()); - copy(value, request.data()); value.setStatus("ACTIVE"); value.setCreatedBy(operatorId); value.setCreatedByName(operatorName); - value.setUpdatedBy(operatorId); value.setUpdatedByName(operatorName); value.setVersion(0); now(value); - try { repository.insert(value); } catch (DuplicateKeyException ex) { throw conflict("渠道代码已存在"); } - return value; + throw conflict("船期渠道来自当前生效船期价格表,不能手工维护"); } @Transactional public SailingChannelEntity updateChannel(long id, long operatorId, String operatorName, ChannelUpdateRequest request) { - SailingChannelEntity before = requiredChannel(id); validateChannel(request.data()); - if (!before.getChannelCode().equalsIgnoreCase(request.data().channelCode().trim()) - && repository.channelByCode(request.data().channelCode().trim().toUpperCase()) != null) throw conflict("渠道代码已存在"); - SailingChannelEntity value = new SailingChannelEntity(); value.setId(id); copy(value, request.data()); - value.setStatus(normalizeStatus(request.status(), "渠道状态")); - if (repository.update(value, version(request.expectedVersion()), operatorId, operatorName) != 1) throw conflict("渠道已被其他人修改"); - return requiredChannel(id); + throw conflict("船期渠道来自当前生效船期价格表,不能手工维护"); } public PageResult templates(TemplateQuery query) { return repository.templates(query); } @@ -73,7 +75,7 @@ public class SailingCalendarService { @Transactional public SailingTemplateEntity createTemplate(long operatorId, String operatorName, TemplateCreateRequest request) { if (version(request.expectedVersion()) != 0) throw bad("新建模板expectedVersion必须为0"); - SailingChannelEntity channel = requiredChannel(request.data().channelId()); validateTemplate(request.data(), channel); + SailingChannelEntity channel = resolvePriceBookChannel(request.data().channelId(), request.data().warehouseId(), null); validateTemplate(request.data(), channel); if (!"ACTIVE".equals(channel.getStatus())) throw conflict("停用渠道不能创建模板"); SailingTemplateEntity value = new SailingTemplateEntity(); value.setId(IdWorker.getId()); value.setTemplateNo("ST-" + value.getId()); copy(value, request.data(), channel); value.setStatus("ACTIVE"); value.setCreatedBy(operatorId); value.setCreatedByName(operatorName); @@ -82,7 +84,7 @@ public class SailingCalendarService { } @Transactional public SailingTemplateEntity updateTemplate(long id, long operatorId, String operatorName, TemplateUpdateRequest request) { - requiredTemplate(id); SailingChannelEntity channel = requiredChannel(request.data().channelId()); validateTemplate(request.data(), channel); + requiredTemplate(id); SailingChannelEntity channel = resolvePriceBookChannel(request.data().channelId(), request.data().warehouseId(), null); validateTemplate(request.data(), channel); SailingTemplateEntity value = new SailingTemplateEntity(); value.setId(id); copy(value, request.data(), channel); value.setStatus(StringUtils.hasText(request.status()) ? normalizeStatus(request.status(), "模板状态") : requiredTemplate(id).getStatus()); if (repository.update(value, version(request.expectedVersion()), operatorId, operatorName) != 1) throw conflict("模板已被其他人修改"); @@ -196,8 +198,8 @@ public class SailingCalendarService { throw bad("预计体积必须大于0"); } String channelCode = trimUpper(request.channelCode()); - SailingChannelEntity channel = StringUtils.hasText(channelCode) ? repository.channelByCode(channelCode) : null; - if (channel == null || !"ACTIVE".equals(channel.getStatus())) throw bad("预报必须填写已启用的渠道编码"); + SailingChannelEntity channel = StringUtils.hasText(channelCode) ? resolvePriceBookChannelByCode(request.receivingWarehouseId(), channelCode) : null; + if (channel == null) throw bad("预报必须填写当前生效船期价格表中的渠道编码"); String departurePort = trimUpper(request.departurePortCode()); String destinationPort = trimUpper(request.destinationPortCode()); if (!StringUtils.hasText(departurePort)) departurePort = trimUpper(channel.getDefaultDeparturePortCode()); @@ -225,8 +227,8 @@ public class SailingCalendarService { if (request.forecastVolumeCbm().signum() <= 0) throw bad("预计体积必须大于0"); String channelCode = trimUpper(request.channelCode()); if (!StringUtils.hasText(channelCode)) channelCode = current.getChannelCode(); - SailingChannelEntity channel = repository.channelByCode(channelCode); - if (channel == null || !"ACTIVE".equals(channel.getStatus())) throw bad("预报必须填写已启用的渠道编码"); + SailingChannelEntity channel = resolvePriceBookChannelByCode(current.getReceivingWarehouseId(), channelCode); + if (channel == null) throw bad("预报必须填写当前生效船期价格表中的渠道编码"); String departurePort = trimUpper(request.departurePortCode()); String destinationPort = trimUpper(request.destinationPortCode()); if (!StringUtils.hasText(departurePort)) departurePort = trimUpper(channel.getDefaultDeparturePortCode()); @@ -391,7 +393,7 @@ public class SailingCalendarService { @Transactional public List generate(long operatorId, String operatorName, long templateId, LocalDate from, LocalDate to) { SailingTemplateEntity template = requiredTemplate(templateId); if (!"ACTIVE".equals(template.getStatus())) throw conflict("停用模板不能生成船期"); - SailingChannelEntity channel = requiredChannel(template.getChannelId()); + SailingChannelEntity channel = resolvePriceBookChannel(template.getChannelId(), template.getWarehouseId(), template.getChannelCodeSnapshot()); if (!"ACTIVE".equals(channel.getStatus())) throw conflict("停用渠道不能生成船期"); validateTemplateSnapshot(template); List result = new java.util.ArrayList<>(); @@ -473,8 +475,33 @@ public class SailingCalendarService { if (d.etaAt() != null && d.etaAt().isBefore(d.etdAt())) throw bad("ETA不能早于ETD"); if (d.cutoffAt() == null || !d.cutoffAt().isBefore(d.etdAt())) throw bad("收货截止必须早于开船时间"); } - private SailingChannelEntity resolveChannel(SailingData d) { if (d.channelId() != null) return requiredChannel(d.channelId()); if (!StringUtils.hasText(d.channelCode())) throw bad("必须选择价格表渠道"); SailingChannelEntity v = repository.channelByCode(d.channelCode().trim().toUpperCase()); if (v == null) throw notFound("渠道不存在"); return v; } - private SailingChannelEntity requiredChannel(long id) { SailingChannelEntity v = repository.channel(id); if (v == null) throw notFound("渠道不存在"); return v; } + private SailingChannelEntity resolveChannel(SailingData d) { + return resolvePriceBookChannel(d.channelId(), d.warehouseId(), d.channelCode()); + } + private SailingChannelEntity resolvePriceBookChannel(Long channelId, Long warehouseId, String channelCode) { + if (warehouseId == null) throw bad("必须选择收货仓后选择价格表渠道"); + Long versionId = priceBooks.activeVersionId(); + if (versionId == null) throw conflict("当前没有生效的船期价格表"); + List rows = priceBooks.channels(versionId, warehouseId); + String normalized = trimUpper(channelCode); + ChannelRow row = rows.stream() + .filter(v -> channelId != null && Objects.equals(v.getChannelCatalogId(), channelId)) + .findFirst() + .orElseGet(() -> rows.stream() + .filter(v -> normalized != null && normalized.equalsIgnoreCase(v.getChannelCode())) + .findFirst().orElse(null)); + if (row == null) throw conflict("渠道必须来自当前生效船期价格表,且在当前收货仓有有效报价"); + return priceBookChannel(row); + } + private SailingChannelEntity resolvePriceBookChannelByCode(Long warehouseId, String channelCode) { + return resolvePriceBookChannel(null, warehouseId, channelCode); + } + private SailingChannelEntity priceBookChannel(ChannelRow row) { + SailingChannelEntity value = new SailingChannelEntity(); + value.setId(row.getChannelCatalogId()); value.setChannelCode(trimUpper(row.getChannelCode())); value.setChannelName(trim(row.getChannelName())); + value.setSourceType("PRICE_BOOK"); value.setDefaultCutoffDays(3); value.setStatus("ACTIVE"); value.setRemark(row.getRemark()); value.setVersion(0); + return value; + } private SailingTemplateEntity requiredTemplate(long id) { SailingTemplateEntity v = repository.template(id); if (v == null) throw notFound("船期模板不存在"); return v; } private SailingEntity requiredSailing(long id) { SailingEntity v = repository.sailing(id); if (v == null) throw notFound("船期不存在"); return v; } private boolean isCopyOf(SailingEntity value, String reason) { diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/discard/controller/CargoDiscardController.java b/lastleg-service/src/main/java/com/baiyue/lastleg/discard/controller/CargoDiscardController.java index 9ba668cdf..0c6552627 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/discard/controller/CargoDiscardController.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/discard/controller/CargoDiscardController.java @@ -87,9 +87,7 @@ public class CargoDiscardController { private void requireApprovalPermission(AuthenticatedUser user) { if (user == null || user.permissions() == null - || !(user.permissions().contains(CargoDiscardService.APPROVAL_PERMISSION) - || user.permissions().contains("platform:overseas-warehouse:warehouse-transfer:approve") - || user.permissions().contains("platform:overseas-warehouse:discard:approve"))) { + || !user.permissions().contains(CargoDiscardService.APPROVAL_PERMISSION)) { throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Cargo discard approval permission is required"); } } diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/discard/dto/CargoDiscardResponses.java b/lastleg-service/src/main/java/com/baiyue/lastleg/discard/dto/CargoDiscardResponses.java index 97098e3ae..1fa1c8e8f 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/discard/dto/CargoDiscardResponses.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/discard/dto/CargoDiscardResponses.java @@ -14,10 +14,10 @@ public final class CargoDiscardResponses { List skus) {} public record CandidateSku(String skuCode, String productName, long availablePackages) {} - public record Summary(Long id, String discardNo, String outboundNo, String inboundCodes, + public record Summary(Long id, String discardNo, String inboundCodes, String overseasWarehouseId, String overseasWarehouseName, int totalPackages, BigDecimal totalFee, String currency, String status, - String createdBy, LocalDateTime createdAt, LocalDateTime reviewedAt, LocalDateTime completedAt) {} + String createdBy, LocalDateTime createdAt, LocalDateTime reviewedAt) {} public record Item(Long id, Long orderId, String inboundCode, String skuCode, String productName, int availablePackagesSnapshot, int discardPackages) {} diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/discard/entity/CargoDiscardApplication.java b/lastleg-service/src/main/java/com/baiyue/lastleg/discard/entity/CargoDiscardApplication.java index 979b8ca67..369ece1f4 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/discard/entity/CargoDiscardApplication.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/discard/entity/CargoDiscardApplication.java @@ -14,7 +14,6 @@ public class CargoDiscardApplication { @TableId(type = IdType.INPUT) private Long id; private String customerId; private String discardNo; - private String outboundNo; private String overseasWarehouseId; private String overseasWarehouseName; private String inboundCodes; @@ -30,7 +29,6 @@ public class CargoDiscardApplication { private String reviewedBy; private String reviewRemark; private LocalDateTime reviewedAt; - private LocalDateTime completedAt; private LocalDateTime createdAt; private LocalDateTime updatedAt; } diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/discard/service/CargoDiscardService.java b/lastleg-service/src/main/java/com/baiyue/lastleg/discard/service/CargoDiscardService.java index 825a89623..c7d1268ef 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/discard/service/CargoDiscardService.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/discard/service/CargoDiscardService.java @@ -16,14 +16,8 @@ import com.baiyue.lastleg.discard.mapper.CargoDiscardInventoryMapper; import com.baiyue.lastleg.discard.mapper.CargoDiscardItemMapper; import com.baiyue.lastleg.discard.mapper.CargoDiscardStatusHistoryMapper; import com.baiyue.lastleg.discard.model.CargoDiscardCandidateRow; -import com.baiyue.lastleg.delivery.entity.OutboundOrder; -import com.baiyue.lastleg.delivery.entity.OutboundPalletItem; -import com.baiyue.lastleg.delivery.mapper.OutboundOrderMapper; -import com.baiyue.lastleg.delivery.mapper.OutboundPalletItemMapper; import com.baiyue.lastleg.dropship.entity.DropshipCarton; import com.baiyue.lastleg.dropship.entity.DropshipSkuInventory; -import com.baiyue.lastleg.dropship.entity.DropshipOrderProfile; -import com.baiyue.lastleg.dropship.mapper.DropshipOrderProfileMapper; import com.baiyue.lastleg.entity.LastlegOrder; import com.baiyue.lastleg.entity.LastlegOrderStatusHistory; import com.baiyue.lastleg.mapper.LastlegOrderMapper; @@ -53,8 +47,6 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; -import java.util.Objects; -import java.util.stream.Collectors; @Service public class CargoDiscardService { @@ -76,9 +68,6 @@ public class CargoDiscardService { @Resource private CargoDiscardInventoryAdjustmentService adjustments; @Resource private LastlegOrderMapper orders; @Resource private LastlegOrderStatusHistoryMapper orderHistories; - @Resource private OutboundOrderMapper outboundOrders; - @Resource private OutboundPalletItemMapper outboundPalletItems; - @Resource private DropshipOrderProfileMapper profiles; @Resource private ReturnInventoryCartonMapper returnCartons; @Resource private ReturnInventoryCartonItemMapper returnCartonItems; @Resource private ObjectMapper objectMapper; @@ -280,10 +269,6 @@ public class CargoDiscardService { release(customerId, links, applicationItems); } - if (APPROVED.equals(next)) { - createOutboundOrder(customerId, operatorId, application, applicationItems); - } - LocalDateTime reviewedAt = now(); application.setStatus(next); application.setReviewedBy(operatorId); application.setReviewRemark(trim(request.remark())); application.setReviewedAt(reviewedAt); @@ -292,9 +277,6 @@ public class CargoDiscardService { if (linked != null) { String linkedStatus = mainOrderStatus(next); linked.setCurrentStatus(linkedStatus); linked.setVersion((linked.getVersion() == null ? 0 : linked.getVersion()) + 1); linked.setUpdatedAt(reviewedAt); - if (APPROVED.equals(next) && StringUtils.hasText(application.getOutboundNo())) { - linked.setOutboundOrderNo(application.getOutboundNo()); - } orders.updateById(linked); LastlegOrderStatusHistory orderHistory = new LastlegOrderStatusHistory(); orderHistory.setId(IdWorker.getId()); orderHistory.setCustomerId(customerId); orderHistory.setOrderId(linked.getId()); orderHistory.setFromStatus(PENDING_REVIEW); @@ -305,68 +287,6 @@ public class CargoDiscardService { return detail(customerId, discardNo); } - private void createOutboundOrder(String customerId, String operatorId, CargoDiscardApplication application, - List applicationItems) { - // Unit tests that exercise the approval state transition without the delivery module - // do not wire these optional persistence collaborators; the running service always does. - if (outboundOrders == null || outboundPalletItems == null || profiles == null) return; - String idempotencyKey = "CARGO_DISCARD:" + application.getId(); - OutboundOrder existing = outboundOrders.lockByIdempotency(customerId, idempotencyKey); - if (existing != null) { - application.setOutboundNo(existing.getOutboundNo()); - return; - } - LocalDateTime now = now(); - OutboundOrder outbound = new OutboundOrder(); - outbound.setId(IdWorker.getId()); - outbound.setCustomerId(customerId); - outbound.setOutboundNo(orderNoGenerator.next("DISCARD_OUTBOUND", application.getOverseasWarehouseId(), outbound.getId())); - outbound.setOverseasWarehouseId(application.getOverseasWarehouseId()); - outbound.setOutboundType("CARGO_DISCARD_OUTBOUND"); - outbound.setStatus("WAIT_PICK"); - outbound.setRecipientInfo("货物废弃:" + application.getDiscardNo()); - outbound.setPackageCount(applicationItems.stream().mapToInt(i -> Objects.requireNonNullElse(i.getDiscardPackages(), 0)).sum()); - outbound.setPalletCount(applicationItems.stream().map(this::physicalKey).collect(Collectors.toSet()).size()); - outbound.setIdempotencyKey(idempotencyKey); - outbound.setCreatedBy(operatorId); - outbound.setCreatedAt(now); - outbound.setUpdatedAt(now); - outboundOrders.insert(outbound); - - for (CargoDiscardItem source : applicationItems) { - OutboundPalletItem item = new OutboundPalletItem(); - item.setId(IdWorker.getId()); - item.setCustomerId(customerId); - item.setOutboundOrderId(outbound.getId()); - item.setPackageCount(Objects.requireNonNullElse(source.getDiscardPackages(), 0)); - item.setPickedPackageCount(0); - item.setStatus("WAIT_PICK"); - item.setSkuCode(source.getSkuCode()); - item.setInboundCode(source.getInboundCode()); - item.setOrderRefs(application.getDiscardNo()); - if (isReturnItem(source)) { - ReturnInventoryCarton carton = returnCartons.selectById(source.getReturnCartonId()); - item.setPalletNo(carton == null ? source.getInboundCode() : firstNonBlank(carton.getPalletCode(), carton.getCartonMark())); - item.setCartonMarks(carton == null ? null : carton.getCartonMark()); - } else { - DropshipOrderProfile profile = profiles.selectById(source.getProfileId()); - if (profile != null) { - item.setWarehousePalletId(profile.getWarehousePalletId()); - item.setPalletNo(firstNonBlank(profile.getPalletNo(), profile.getSourcePalletNo())); - } - } - item.setCreatedAt(now); - outboundPalletItems.insert(item); - } - application.setOutboundNo(outbound.getOutboundNo()); - application.setUpdatedAt(now); - applications.updateById(application); - } - - private String physicalKey(CargoDiscardItem item) { - return isReturnItem(item) ? "RETURN:" + item.getReturnCartonId() : "DROPSHIP:" + item.getProfileId(); - } - @Transactional public CargoDiscardResponses.Detail complete(String customerId, String discardNo, String operatorId) { CargoDiscardApplication application = applications.lockNo(customerId, discardNo); @@ -379,9 +299,7 @@ public class CargoDiscardService { public void updateLinkedStatus(String customerId, CargoDiscardApplication application, String from, String to, String operatorId, String remark) { - LocalDateTime at = now(); application.setStatus(to); application.setUpdatedAt(at); - if (COMPLETED.equals(to)) application.setCompletedAt(at); - applications.updateById(application); + LocalDateTime at = now(); application.setStatus(to); application.setUpdatedAt(at); applications.updateById(application); LastlegOrder linked = orders.findSource(customerId, "LASTLEG_DISCARD", "CARGO_DISCARD:" + application.getId()); if (linked != null) { String linkedFrom = mainOrderStatus(from); @@ -502,26 +420,22 @@ public class CargoDiscardService { return "RETURN_CARTON".equals(item.getSourceType()) || item.getReturnCartonId() != null; } - private static String firstNonBlank(String first, String second) { - return first != null && !first.isBlank() ? first : second; - } - private static long value(Long value) { return value == null ? 0L : value; } private LastlegOrder requireDropshipOrder(String customerId, Long orderId) { LastlegOrder order = orders.selectById(orderId); if (order == null || !customerId.equals(order.getCustomerId())) throw notFound("Inbound order not found"); - if (!StringUtils.hasText(order.getOverseasWarehouseId())) { - throw conflict("Cargo discard inventory is not linked to an overseas warehouse"); + if (!"ONE_CLICK_DROPSHIP".equals(order.getOrderType())) { + throw conflict("Cargo discard currently requires SKU/carton inventory from a one-click dropship order"); } return order; } private CargoDiscardResponses.Summary summary(CargoDiscardApplication value) { - return new CargoDiscardResponses.Summary(value.getId(), value.getDiscardNo(), value.getOutboundNo(), value.getInboundCodes(), + return new CargoDiscardResponses.Summary(value.getId(), value.getDiscardNo(), value.getInboundCodes(), value.getOverseasWarehouseId(), value.getOverseasWarehouseName(), value.getTotalPackages(), value.getTotalFee(), value.getCurrency(), value.getStatus(), value.getCreatedBy(), value.getCreatedAt(), - value.getReviewedAt(), value.getCompletedAt()); + value.getReviewedAt()); } private void history(String customerId, Long applicationId, String from, String to, String operatorId, diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/relabel/dto/RelabelRequests.java b/lastleg-service/src/main/java/com/baiyue/lastleg/relabel/dto/RelabelRequests.java index 3947eaf58..4595244e2 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/relabel/dto/RelabelRequests.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/relabel/dto/RelabelRequests.java @@ -122,10 +122,7 @@ public final class RelabelRequests { public record Query(@Min(1) long page, @Min(1) @Max(200) long size, String relabelNo, String customerOrderNo, String skuCode, String status, - String overseasWarehouseId, LocalDateTime createdFrom, LocalDateTime createdTo, - LocalDateTime reviewedFrom, LocalDateTime reviewedTo, - LocalDateTime completedFrom, LocalDateTime completedTo, - LocalDateTime outboundFrom, LocalDateTime outboundTo) { + String overseasWarehouseId, LocalDateTime createdFrom, LocalDateTime createdTo) { public Query { if (page == 0) page = 1; if (size == 0) size = 20; } } diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/relabel/dto/RelabelResponses.java b/lastleg-service/src/main/java/com/baiyue/lastleg/relabel/dto/RelabelResponses.java index db76931eb..9e8d44564 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/relabel/dto/RelabelResponses.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/relabel/dto/RelabelResponses.java @@ -18,8 +18,7 @@ public final class RelabelResponses { String walletErrorMessage, String deliveryMethod, boolean printShippingLabel, String shippingLabelFileId, String shippingLabelFileName, String shippingLabelFileUrl, String fulfillmentStatus, String outboundOrderNo, LocalDateTime customerConfirmedAt, - String reviewChannel, String reviewRemark, LocalDateTime reviewedAt, - LocalDateTime relabelCompletedAt, LocalDateTime outboundAt, LocalDateTime createdAt) {} + String reviewChannel, String reviewRemark, LocalDateTime createdAt) {} public record Item(Long id, String skuCode, String productImageFileId, String productImageFileName, String productImageUrl, String productNameZh, int totalPackageCount, int notifiedPackageCount, String shipmentNo, String trackingCode, diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/relabel/service/RelabelApplicationService.java b/lastleg-service/src/main/java/com/baiyue/lastleg/relabel/service/RelabelApplicationService.java index ed620709b..f91799008 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/relabel/service/RelabelApplicationService.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/relabel/service/RelabelApplicationService.java @@ -502,12 +502,6 @@ public class RelabelApplicationService implements WalletResultHandler { trim(query.overseasWarehouseId())) .ge(query.createdFrom() != null, RelabelApplication::getCreatedAt, query.createdFrom()) .le(query.createdTo() != null, RelabelApplication::getCreatedAt, query.createdTo()) - .apply(query.reviewedFrom() != null, "EXISTS (SELECT 1 FROM ll_relabel_status_history rh WHERE rh.application_id=ll_relabel_application.id AND rh.customer_id=ll_relabel_application.customer_id AND rh.to_status IN ('PENDING_PROCESSING','PROCESSING','REJECTED') AND rh.created_at >= {0})", query.reviewedFrom()) - .apply(query.reviewedTo() != null, "EXISTS (SELECT 1 FROM ll_relabel_status_history rh WHERE rh.application_id=ll_relabel_application.id AND rh.customer_id=ll_relabel_application.customer_id AND rh.to_status IN ('PENDING_PROCESSING','PROCESSING','REJECTED') AND rh.created_at <= {0})", query.reviewedTo()) - .apply(query.completedFrom() != null, "EXISTS (SELECT 1 FROM ll_relabel_pda_task pt WHERE pt.application_id=ll_relabel_application.id AND pt.customer_id=ll_relabel_application.customer_id AND pt.reviewed_at >= {0})", query.completedFrom()) - .apply(query.completedTo() != null, "EXISTS (SELECT 1 FROM ll_relabel_pda_task pt WHERE pt.application_id=ll_relabel_application.id AND pt.customer_id=ll_relabel_application.customer_id AND pt.reviewed_at <= {0})", query.completedTo()) - .apply(query.outboundFrom() != null, "EXISTS (SELECT 1 FROM ll_outbound_order oo WHERE oo.customer_id=ll_relabel_application.customer_id AND oo.outbound_no=ll_relabel_application.outbound_order_no AND oo.created_at >= {0})", query.outboundFrom()) - .apply(query.outboundTo() != null, "EXISTS (SELECT 1 FROM ll_outbound_order oo WHERE oo.customer_id=ll_relabel_application.customer_id AND oo.outbound_no=ll_relabel_application.outbound_order_no AND oo.created_at <= {0})", query.outboundTo()) .apply(StringUtils.hasText(query.skuCode()), "EXISTS (SELECT 1 FROM ll_relabel_item ri WHERE ri.application_id=ll_relabel_application.id AND ri.customer_id=ll_relabel_application.customer_id AND ri.sku_code LIKE CONCAT('%',{0},'%'))", query.skuCode()) @@ -1055,24 +1049,8 @@ public class RelabelApplicationService implements WalletResultHandler { value.getShippingLabelFileId(), value.getShippingLabelFileName(), value.getShippingLabelFileUrl(), value.getFulfillmentStatus(), value.getOutboundOrderNo(), value.getCustomerConfirmedAt(), order != null && "RELABEL_APPLICATION".equals(order.getOrderType()) ? "TAIL_ORDER" : "RELABEL_ORDER", - null, reviewTime(value), completionTime(value), outboundTime(value), value.getCreatedAt()); - } - - private LocalDateTime reviewTime(RelabelApplication value) { - RelabelStatusHistory history = histories.selectOne(new LambdaQueryWrapper() - .eq(RelabelStatusHistory::getApplicationId, value.getId()).eq(RelabelStatusHistory::getCustomerId, value.getCustomerId()) - .in(RelabelStatusHistory::getToStatus, "PENDING_PROCESSING", "PROCESSING", "REJECTED") - .orderByDesc(RelabelStatusHistory::getCreatedAt).last("LIMIT 1")); - return history == null ? null : history.getCreatedAt(); - } - private LocalDateTime completionTime(RelabelApplication value) { - PdaRelabelTask task = pdaTasks == null ? null : pdaTasks.find(value.getCustomerId(), value.getId()); - return task == null ? null : task.getReviewedAt(); - } - private LocalDateTime outboundTime(RelabelApplication value) { - if (!StringUtils.hasText(value.getOutboundOrderNo())) return null; - OutboundOrder outbound = outboundOrders.findByNo(value.getCustomerId(), value.getOutboundOrderNo()); - return outbound == null ? null : outbound.getCreatedAt(); + null, + value.getCreatedAt()); } @Override public boolean supports(String businessType) { return "RELABEL_ORDER".equals(businessType); } diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/controller/MerchantReturnOrderController.java b/lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/controller/MerchantReturnOrderController.java index d83bcb4d2..09e870bb6 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/controller/MerchantReturnOrderController.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/controller/MerchantReturnOrderController.java @@ -24,11 +24,6 @@ public class MerchantReturnOrderController { @PostMapping @PreAuthorize("hasAuthority('" + OverseasPermissions.MERCHANT_RETURN_ORDER_CREATE + "')") public ApiResponse create(@AuthenticationPrincipal AuthenticatedUser user, @Valid @RequestBody ReturnOrderRequests.Create request) { return ApiResponse.success(service.create(customer(user), operator(user), request)); } - @PutMapping("/{returnNo}") @PreAuthorize("hasAuthority('" + OverseasPermissions.MERCHANT_RETURN_ORDER_CREATE + "')") - public ApiResponse update(@AuthenticationPrincipal AuthenticatedUser user, - @PathVariable String returnNo, @Valid @RequestBody ReturnOrderRequests.Create request) { - return ApiResponse.success(service.updatePending(customer(user), returnNo, request)); - } @GetMapping @PreAuthorize("hasAuthority('" + OverseasPermissions.MERCHANT_RETURN_ORDER_LIST + "')") public ApiResponse> page(@AuthenticationPrincipal AuthenticatedUser user, @Valid ReturnOrderRequests.Query query) { return ApiResponse.success(service.page(customer(user), query)); } diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/transfer/dto/WarehouseTransferRequests.java b/lastleg-service/src/main/java/com/baiyue/lastleg/transfer/dto/WarehouseTransferRequests.java index 4e3c3f8ec..42804efba 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/transfer/dto/WarehouseTransferRequests.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/transfer/dto/WarehouseTransferRequests.java @@ -168,10 +168,7 @@ public final class WarehouseTransferRequests { public record Query(@Min(1) long page, @Min(1) @Max(200) long size, String transferNo, String inboundCode, String operatingWarehouseId, String fbaWarehouseCode, String status, Boolean needRelabel, - LocalDateTime createdFrom, LocalDateTime createdTo, - LocalDateTime reviewedFrom, LocalDateTime reviewedTo, - LocalDateTime measuredFrom, LocalDateTime measuredTo, - LocalDateTime outboundFrom, LocalDateTime outboundTo) { + LocalDateTime createdFrom, LocalDateTime createdTo) { public Query { if (page == 0) page = 1; if (size == 0) size = 20; } } } diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/transfer/dto/WarehouseTransferResponses.java b/lastleg-service/src/main/java/com/baiyue/lastleg/transfer/dto/WarehouseTransferResponses.java index d7334bf94..882719a4c 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/transfer/dto/WarehouseTransferResponses.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/transfer/dto/WarehouseTransferResponses.java @@ -31,9 +31,7 @@ public final class WarehouseTransferResponses { String destinationType, String destinationWarehouseCode, String destinationWarehouseName, int packageCount, String deliveryMethod, boolean needRelabel, String relabelNo, String relabelApplicationStatus, String relabelFulfillmentStatus, - String status, LocalDateTime estimatedArrivalAt, LocalDateTime estimatedShipAt, - LocalDateTime actualShipAt, LocalDateTime reviewedAt, LocalDateTime measuredAt, - LocalDateTime outboundAt, LocalDateTime relabelCompletedAt, LocalDateTime createdAt) {} + String status, LocalDateTime estimatedArrivalAt, LocalDateTime createdAt) {} public record Detail(Long id, String transferNo, Long inboundOrderId, String inboundCode, Long lastlegOrderId, String lastlegOrderNo, String orderType, String orderStatus, String status, diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/transfer/entity/WarehouseTransferOrder.java b/lastleg-service/src/main/java/com/baiyue/lastleg/transfer/entity/WarehouseTransferOrder.java index 2356042cc..a994d1730 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/transfer/entity/WarehouseTransferOrder.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/transfer/entity/WarehouseTransferOrder.java @@ -56,8 +56,6 @@ public class WarehouseTransferOrder { private String outboundIdempotencyKey; private String outboundRequestHash; private LocalDateTime outboundAt; - private LocalDateTime measuredAt; - private LocalDateTime actualShipAt; private String palletizeIdempotencyKey; private String palletizeRequestHash; private String confirmationIdempotencyKey; diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/transfer/mapper/WarehouseTransferOrderMapper.java b/lastleg-service/src/main/java/com/baiyue/lastleg/transfer/mapper/WarehouseTransferOrderMapper.java index 784e08681..bf0be19c6 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/transfer/mapper/WarehouseTransferOrderMapper.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/transfer/mapper/WarehouseTransferOrderMapper.java @@ -20,10 +20,10 @@ import org.apache.ibatis.annotations.Update; WarehouseTransferOrder lockByRelabelNo(@Param("customerId") String customerId, @Param("relabelNo") String relabelNo); @Select("SELECT * FROM ll_warehouse_transfer_order WHERE operating_warehouse_id=#{warehouseId} AND status='WAIT_PDA_RECEIVING' ORDER BY created_at,id") java.util.List pdaCandidates(@Param("warehouseId") String warehouseId); - @Update("UPDATE ll_warehouse_transfer_order SET status='OUTBOUNDED',outbound_at=#{outboundAt},actual_ship_at=#{outboundAt},updated_at=#{outboundAt} WHERE customer_id=#{customerId} AND lastleg_order_id=#{orderId} AND outbound_at IS NULL") + @Update("UPDATE ll_warehouse_transfer_order SET status='OUTBOUNDED',outbound_at=#{outboundAt},updated_at=#{outboundAt} WHERE customer_id=#{customerId} AND lastleg_order_id=#{orderId} AND outbound_at IS NULL") int markOutboundByOrder(@Param("customerId") String customerId, @Param("orderId") Long orderId, @Param("outboundAt") java.time.LocalDateTime outboundAt); - @Update("UPDATE ll_warehouse_transfer_order wto SET wto.status='OUTBOUNDED',wto.outbound_at=#{outboundAt},wto.actual_ship_at=#{outboundAt},wto.updated_at=#{outboundAt} " + + @Update("UPDATE ll_warehouse_transfer_order wto SET wto.status='OUTBOUNDED',wto.outbound_at=#{outboundAt},wto.updated_at=#{outboundAt} " + "WHERE wto.customer_id=#{customerId} AND wto.relabel_no=#{relabelNo} AND wto.outbound_at IS NULL " + "AND NOT EXISTS (SELECT 1 FROM ll_warehouse_transfer_pallet wtp " + "JOIN ll_inventory_position ip ON ip.customer_id=wtp.customer_id AND ip.pallet_id=wtp.warehouse_pallet_id " + diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/transfer/service/WarehouseTransferService.java b/lastleg-service/src/main/java/com/baiyue/lastleg/transfer/service/WarehouseTransferService.java index 30be44835..f4c8b71fe 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/transfer/service/WarehouseTransferService.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/transfer/service/WarehouseTransferService.java @@ -147,6 +147,12 @@ public class WarehouseTransferService { validateDestination(request.destinationType(), request.destinationWarehouseCode(), request.fbaWarehouseCode(), request.privateDestinationAddress(), request.privateDestinationPostalCode(), request.privateDestinationContactName(), request.privateDestinationContactPhone()); + // A split relabel transfer may intentionally defer the whole-order + // destination: relabeled cartons go through relabel dispatch while the + // remaining cartons are bound later from warehouse-transfer outbound. + // All other transfer types must still select FBA or PRIVATE up front. + validateTransferDestinationRequired(request.destinationType(), request.fbaWarehouseCode(), + request.needRelabel() && request.relabelItems() != null && !request.relabelItems().isEmpty()); String key = request.idempotencyKey().trim(); String requestHash = hash(request); WarehouseTransferOrder replay = transfers.findKey(customerId, key); @@ -266,8 +272,8 @@ public class WarehouseTransferService { WarehouseTransferRequests.Create request) { WarehouseTransferOrder transfer = transfers.lockNo(customerId, transferNo); if (transfer == null) throw notFound("Warehouse transfer order not found"); - if (!Set.of(DRAFT, REJECTED).contains(transfer.getStatus())) { - throw conflict("Only a draft or rejected warehouse transfer order can be edited"); + if (!DRAFT.equals(transfer.getStatus())) { + throw conflict("Only a draft warehouse transfer order can be edited"); } int itemPackages = request.items().stream().mapToInt(WarehouseTransferRequests.Item::packageCount).sum(); if (itemPackages != request.packageCount()) { @@ -277,6 +283,8 @@ public class WarehouseTransferService { validateDestination(request.destinationType(), request.destinationWarehouseCode(), request.fbaWarehouseCode(), request.privateDestinationAddress(), request.privateDestinationPostalCode(), request.privateDestinationContactName(), request.privateDestinationContactPhone()); + validateTransferDestinationRequired(request.destinationType(), request.fbaWarehouseCode(), + request.needRelabel() && request.relabelItems() != null && !request.relabelItems().isEmpty()); LastlegOrder order = requireTailOrder(customerId, transfer); LocalDateTime now = LocalDateTime.now(); @@ -312,7 +320,7 @@ public class WarehouseTransferService { order.setVolumeCbm(request.volumeCbm()); order.setEstimatedWarehouseAt(request.estimatedArrivalAt()); order.setRelabelStatus(request.needRelabel() ? "PENDING_BINDING" : "NOT_REQUIRED"); - order.setSubOrderStatus("WAREHOUSE_TRANSFER:" + transfer.getStatus() + ";RELABEL:" + order.getRelabelStatus()); + order.setSubOrderStatus("WAREHOUSE_TRANSFER:" + DRAFT + ";RELABEL:" + order.getRelabelStatus()); order.setCustomerRemark(trim(request.remark())); order.setSourcePayloadHash("sha256:" + requestHash); order.setUpdatedAt(now); @@ -365,10 +373,7 @@ public class WarehouseTransferService { WarehouseTransferOrder transfer = transfers.lockNo(customerId, transferNo); if (transfer == null) throw notFound("Warehouse transfer order not found"); if (PENDING_REVIEW.equals(transfer.getStatus())) return detail(customerId, transferNo); - String previousStatus = transfer.getStatus(); - if (!Set.of(DRAFT, REJECTED).contains(previousStatus)) { - throw conflict("Only a draft or rejected warehouse transfer order can be submitted"); - } + if (!DRAFT.equals(transfer.getStatus())) throw conflict("Only a draft warehouse transfer order can be submitted"); LastlegOrder order = requireTailOrder(customerId, transfer); LocalDateTime now = LocalDateTime.now(); @@ -383,13 +388,12 @@ public class WarehouseTransferService { LastlegInboundOrder inbound = transfer.getTransferNo().equals(transfer.getInboundCode()) ? inboundOrders.selectById(transfer.getInboundOrderId()) : null; - if (inbound != null && customerId.equals(inbound.getCustomerId()) - && Set.of(DRAFT, REJECTED, "CANCELLED").contains(inbound.getStatus())) { + if (inbound != null && customerId.equals(inbound.getCustomerId()) && DRAFT.equals(inbound.getStatus())) { inbound.setStatus(PENDING_REVIEW); inbound.setUpdatedAt(now); inboundOrders.updateById(inbound); } - insertOrderHistory(customerId, order.getId(), previousStatus, PENDING_REVIEW, transferNo, + insertOrderHistory(customerId, order.getId(), DRAFT, PENDING_REVIEW, transferNo, "Warehouse transfer submitted for review by " + operatorId, now); return detail(customerId, transferNo); } @@ -420,12 +424,6 @@ public class WarehouseTransferService { .eq(query.needRelabel() != null, WarehouseTransferOrder::getNeedRelabel, query.needRelabel()) .ge(query.createdFrom() != null, WarehouseTransferOrder::getCreatedAt, query.createdFrom()) .le(query.createdTo() != null, WarehouseTransferOrder::getCreatedAt, query.createdTo()) - .ge(query.reviewedFrom() != null, WarehouseTransferOrder::getReviewedAt, query.reviewedFrom()) - .le(query.reviewedTo() != null, WarehouseTransferOrder::getReviewedAt, query.reviewedTo()) - .ge(query.measuredFrom() != null, WarehouseTransferOrder::getMeasuredAt, query.measuredFrom()) - .le(query.measuredTo() != null, WarehouseTransferOrder::getMeasuredAt, query.measuredTo()) - .ge(query.outboundFrom() != null, WarehouseTransferOrder::getOutboundAt, query.outboundFrom()) - .le(query.outboundTo() != null, WarehouseTransferOrder::getOutboundAt, query.outboundTo()) .orderByDesc(WarehouseTransferOrder::getCreatedAt).orderByDesc(WarehouseTransferOrder::getId); Page result = transfers.selectPage(Page.of(query.page(), query.size()), wrapper); Map relabelApplicationsByNo = relabelApplications(result.getRecords()); @@ -438,9 +436,7 @@ public class WarehouseTransferService { relabelApplicationsByNo.get(value.getRelabelNo()) == null ? null : relabelApplicationsByNo.get(value.getRelabelNo()).getStatus(), relabelApplicationsByNo.get(value.getRelabelNo()) == null ? null : relabelApplicationsByNo.get(value.getRelabelNo()).getFulfillmentStatus(), value.getStatus(), - value.getEstimatedArrivalAt(), value.getEstimatedShipAt(), value.getActualShipAt(), - value.getReviewedAt(), value.getMeasuredAt(), value.getOutboundAt(), value.getRelabelCompletedAt(), - value.getCreatedAt())).toList(); + value.getEstimatedArrivalAt(), value.getCreatedAt())).toList(); return new PageResult<>(records, result.getCurrent(), result.getSize(), result.getTotal(), result.getPages()); } @@ -858,7 +854,7 @@ public class WarehouseTransferService { completedPalletItems, warehouse, now); completeTransferInboundSnapshot(customerId, transfer, persistedPallets, warehouse, now); completeTransferInboundPlan(customerId, transfer, persistedPallets, completedPalletItems, now); - transfer.setStatus(WAIT_CUSTOMER_CONFIRMATION); transfer.setMeasuredAt(now); transfer.setPalletizeIdempotencyKey(request.idempotencyKey().trim()); + transfer.setStatus(WAIT_CUSTOMER_CONFIRMATION); transfer.setPalletizeIdempotencyKey(request.idempotencyKey().trim()); transfer.setPalletizeRequestHash(requestHash); transfer.setUpdatedAt(now); transfers.updateById(transfer); LastlegOrder order = requireTailOrder(customerId, transfer); String before = order.getCurrentStatus(); order.setCurrentStatus(INBOUND_COMPLETED); order.setPalletCount(persistedPallets.size()); @@ -1248,9 +1244,7 @@ public class WarehouseTransferService { relabel == null ? null : relabel.getStatus(), relabel == null ? null : relabel.getFulfillmentStatus(), value.getStatus(), - value.getEstimatedArrivalAt(), value.getEstimatedShipAt(), value.getActualShipAt(), - value.getReviewedAt(), value.getMeasuredAt(), value.getOutboundAt(), value.getRelabelCompletedAt(), - value.getCreatedAt()); + value.getEstimatedArrivalAt(), value.getCreatedAt()); }).toList(); } @@ -1430,6 +1424,15 @@ public class WarehouseTransferService { } } + private static void validateTransferDestinationRequired(String destinationType, String fbaWarehouseCode, + boolean allowDeferredForRelabel) { + String type = trim(destinationType); + if (type == null && StringUtils.hasText(fbaWarehouseCode)) type = "FBA"; + if ((type == null || "NONE".equalsIgnoreCase(type)) && !allowDeferredForRelabel) { + throw badRequest("仓储转运必须选择 FBA 仓或私人仓"); + } + } + private static void applyDestination(WarehouseTransferOrder transfer, WarehouseTransferRequests.Create request) { String type = trim(request.destinationType()); if (type == null && StringUtils.hasText(request.fbaWarehouseCode())) type = "FBA"; diff --git a/lastleg-service/src/main/resources/db/lastleg.sql b/lastleg-service/src/main/resources/db/lastleg.sql index 5e82121fb..a558dba68 100644 --- a/lastleg-service/src/main/resources/db/lastleg.sql +++ b/lastleg-service/src/main/resources/db/lastleg.sql @@ -1628,10 +1628,6 @@ SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE tab PREPARE transfer_outbound_hash_stmt FROM @ddl; EXECUTE transfer_outbound_hash_stmt; DEALLOCATE PREPARE transfer_outbound_hash_stmt; SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_warehouse_transfer_order' AND column_name='outbound_at'), 'SELECT 1', 'ALTER TABLE ll_warehouse_transfer_order ADD COLUMN outbound_at DATETIME(3) NULL COMMENT ''出库时间'' AFTER outbound_request_hash')); PREPARE transfer_outbound_at_stmt FROM @ddl; EXECUTE transfer_outbound_at_stmt; DEALLOCATE PREPARE transfer_outbound_at_stmt; -SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_warehouse_transfer_order' AND column_name='measured_at'), 'SELECT 1', 'ALTER TABLE ll_warehouse_transfer_order ADD COLUMN measured_at DATETIME(3) NULL COMMENT ''PDA测量完成时间'' AFTER outbound_at')); -PREPARE transfer_measured_at_stmt FROM @ddl; EXECUTE transfer_measured_at_stmt; DEALLOCATE PREPARE transfer_measured_at_stmt; -SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_warehouse_transfer_order' AND column_name='actual_ship_at'), 'SELECT 1', 'ALTER TABLE ll_warehouse_transfer_order ADD COLUMN actual_ship_at DATETIME(3) NULL COMMENT ''实际发货时间'' AFTER measured_at')); -PREPARE transfer_actual_ship_at_stmt FROM @ddl; EXECUTE transfer_actual_ship_at_stmt; DEALLOCATE PREPARE transfer_actual_ship_at_stmt; SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_warehouse_transfer_order' AND column_name='estimated_ship_at'), 'SELECT 1', 'ALTER TABLE ll_warehouse_transfer_order ADD COLUMN estimated_ship_at DATETIME(3) NULL COMMENT ''预计发货时间'' AFTER estimated_arrival_at')); PREPARE transfer_estimated_ship_stmt FROM @ddl; EXECUTE transfer_estimated_ship_stmt; DEALLOCATE PREPARE transfer_estimated_ship_stmt; SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_warehouse_transfer_order' AND column_name='pickup_contact_name'), 'SELECT 1', 'ALTER TABLE ll_warehouse_transfer_order ADD COLUMN pickup_contact_name VARCHAR(128) NULL COMMENT ''提货联系人'' AFTER delivery_address')); @@ -2060,7 +2056,7 @@ CREATE TABLE IF NOT EXISTS ll_dropship_carton_ledger ( CREATE TABLE IF NOT EXISTS ll_cargo_discard_application ( id BIGINT NOT NULL COMMENT '主键ID', customer_id VARCHAR(128) NOT NULL COMMENT '客户租户ID', - discard_no VARCHAR(64) NOT NULL COMMENT '废弃申请单号', outbound_no VARCHAR(64) NULL COMMENT '废弃出库单号', overseas_warehouse_id VARCHAR(128) NOT NULL COMMENT '海外仓ID', + discard_no VARCHAR(64) NOT NULL COMMENT '废弃申请单号', overseas_warehouse_id VARCHAR(128) NOT NULL COMMENT '海外仓ID', overseas_warehouse_name VARCHAR(255) NULL COMMENT '海外仓名称快照', inbound_codes VARCHAR(2000) NOT NULL COMMENT '入仓单号集合', total_packages INT NOT NULL COMMENT '总箱数', fee_per_package DECIMAL(18,2) NOT NULL COMMENT '每箱处理费用', total_fee DECIMAL(18,2) NOT NULL COMMENT '费用合计', currency CHAR(3) NOT NULL DEFAULT 'USD' COMMENT '币种', @@ -2068,7 +2064,7 @@ CREATE TABLE IF NOT EXISTS ll_cargo_discard_application ( idempotency_key VARCHAR(255) NOT NULL COMMENT '业务幂等键', request_hash CHAR(64) NOT NULL COMMENT '请求内容哈希', created_by VARCHAR(128) NULL COMMENT '创建人ID', reviewed_by VARCHAR(128) NULL COMMENT '审核人ID', review_remark VARCHAR(1000) NULL COMMENT '审核备注', review_idempotency_key VARCHAR(255) NULL COMMENT '审核幂等键', review_request_hash CHAR(64) NULL COMMENT '审核请求内容哈希', - reviewed_at DATETIME(3) NULL COMMENT '审核时间', completed_at DATETIME(3) NULL COMMENT '废弃完成时间', created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间', + reviewed_at DATETIME(3) 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_ll_cargo_discard_no(customer_id,discard_no), UNIQUE KEY uk_ll_cargo_discard_key(customer_id,idempotency_key), diff --git a/lastleg-service/src/main/resources/openapi/overseas-business-v1.yaml b/lastleg-service/src/main/resources/openapi/overseas-business-v1.yaml index cc4c5a626..9f7836de1 100644 --- a/lastleg-service/src/main/resources/openapi/overseas-business-v1.yaml +++ b/lastleg-service/src/main/resources/openapi/overseas-business-v1.yaml @@ -1556,13 +1556,6 @@ paths: security: [{ bearerAuth: [] }] parameters: [{ name: returnNo, in: path, required: true, schema: { type: string } }] responses: { '200': { description: 当前客户退货订单详情 }, '404': { description: 当前客户下退货订单不存在 } } - put: - summary: 客户编辑待审核退货预报 - description: 权限 merchant:overseas-warehouse:return-order:create;仅允许修改当前客户且仍处于 PENDING_REVIEW 的退货预报,SKU 预报明细会整体替换。 - security: [{ bearerAuth: [] }] - parameters: [{ name: returnNo, in: path, required: true, schema: { type: string } }] - requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/ReturnOrderCreate' } } } } - responses: { '200': { description: 更新后的退货订单详情 }, '404': { description: 当前客户下退货订单不存在 }, '409': { description: 退货预报已不处于待审核状态 } } /api/merchant/v1/last-leg/return-orders/{returnNo}/photos/files/{fileId}/download: get: summary: 客户下载退货签收打托照片 diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/merchant/MerchantOverseasCustomerApiContractTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/merchant/MerchantOverseasCustomerApiContractTest.java index f12eb5cb3..c547cb562 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/merchant/MerchantOverseasCustomerApiContractTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/merchant/MerchantOverseasCustomerApiContractTest.java @@ -85,7 +85,6 @@ class MerchantOverseasCustomerApiContractTest { "cancel", OverseasPermissions.MERCHANT_DISCARD_CANCEL)); assertPermissions(MerchantReturnOrderController.class, Map.of( "create", OverseasPermissions.MERCHANT_RETURN_ORDER_CREATE, - "update", OverseasPermissions.MERCHANT_RETURN_ORDER_CREATE, "page", OverseasPermissions.MERCHANT_RETURN_ORDER_LIST, "detail", OverseasPermissions.MERCHANT_RETURN_ORDER_LIST, "downloadPhoto", OverseasPermissions.MERCHANT_RETURN_ORDER_LIST, diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/relabel/controller/MerchantRelabelApplicationControllerTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/relabel/controller/MerchantRelabelApplicationControllerTest.java index ed1a149e5..dce3a6aab 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/relabel/controller/MerchantRelabelApplicationControllerTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/relabel/controller/MerchantRelabelApplicationControllerTest.java @@ -17,8 +17,7 @@ class MerchantRelabelApplicationControllerTest { private final RelabelApplicationService service = mock(RelabelApplicationService.class); private final MerchantRelabelApplicationController controller = controller(); private final RelabelRequests.Query query = new RelabelRequests.Query( - 1, 20, null, null, null, null, null, null, null, - null, null, null, null, null, null); + 1, 20, null, null, null, null, null, null, null); @Test void merchantPageAndDetailUseOnlyAuthenticatedTenant() { diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/transfer/controller/MerchantWarehouseTransferControllerTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/transfer/controller/MerchantWarehouseTransferControllerTest.java index 91761ec10..ef731e388 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/transfer/controller/MerchantWarehouseTransferControllerTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/transfer/controller/MerchantWarehouseTransferControllerTest.java @@ -17,8 +17,7 @@ class MerchantWarehouseTransferControllerTest { private final WarehouseTransferService service = mock(WarehouseTransferService.class); private final MerchantWarehouseTransferController controller = controller(); private final WarehouseTransferRequests.Query query = new WarehouseTransferRequests.Query( - 1, 20, null, null, null, null, null, null, null, null, - null, null, null, null, null, null); + 1, 20, null, null, null, null, null, null, null, null); @Test void merchantPageUsesOnlyAuthenticatedTenant() { diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/transfer/service/WarehouseTransferServiceTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/transfer/service/WarehouseTransferServiceTest.java index 4fa0efa85..864315768 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/transfer/service/WarehouseTransferServiceTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/transfer/service/WarehouseTransferServiceTest.java @@ -120,8 +120,7 @@ class WarehouseTransferServiceTest { when(relabelApplications.selectList(any())).thenReturn(List.of(relabel)); var result = service.page("TENANT-A", new WarehouseTransferRequests.Query( - 1, 20, null, null, null, null, null, null, null, null, - null, null, null, null, null, null)); + 1, 20, null, null, null, null, null, null, null, null)); assertEquals("RL-20260923-000001", result.records().get(0).relabelNo()); assertEquals("COMPLETED", result.records().get(0).relabelApplicationStatus()); @@ -388,25 +387,19 @@ class WarehouseTransferServiceTest { } @Test - void transferAllowsRequestWithoutDestinationForLaterBinding() { + void transferRejectsRequestWithoutDestination() { WarehouseTransferRequests.Create request = new WarehouseTransferRequests.Create( "IN-1", List.of(11L), "WH-1", null, "123 Pickup Road", 5, new BigDecimal("0.2500"), "WAREHOUSE_PICKUP", LocalDateTime.of(2026, 9, 12, 10, 0), null, null, null, "FBA destination will be supplied later", false, "key-no-fba", List.of(new WarehouseTransferRequests.Item("SKU-1", "Product", 5, "MARK-1"))); - when(inboundSnapshots.findId("TENANT-A", "IN-1")).thenReturn(101L); - when(orders.selectList(any())).thenReturn(List.of(historicalOrder())); - when(transfers.findNo(eq("TENANT-A"), anyString())).thenAnswer(invocation -> transfer(WarehouseTransferService.DRAFT)); - when(transferItems.selectList(any())).thenReturn(List.of()); - when(sourceOrders.selectList(any())).thenReturn(List.of()); + ResponseStatusException exception = assertThrows(ResponseStatusException.class, + () -> service.create("TENANT-A", "USER-1", request)); - var result = service.create("TENANT-A", "USER-1", request); - - assertEquals(WarehouseTransferService.DRAFT, result.status()); - ArgumentCaptor transfer = ArgumentCaptor.forClass(WarehouseTransferOrder.class); - verify(transfers).insert(transfer.capture()); - assertNull(transfer.getValue().getDestinationType()); + assertEquals(400, exception.getStatusCode().value()); + assertEquals("仓储转运必须选择 FBA 仓或私人仓", exception.getReason()); + verifyNoInteractions(transfers); } @Test @@ -618,53 +611,6 @@ class WarehouseTransferServiceTest { verify(orderDetails).insert(any(LastlegOrderDetail.class)); } - @Test - void merchantUpdatesRejectedTransferBeforeResubmission() { - WarehouseTransferOrder transfer = persistedTransfer(); - transfer.setStatus(WarehouseTransferService.REJECTED); - LastlegOrder tailOrder = historicalOrder(); - tailOrder.setId(31L); tailOrder.setCustomerId("TENANT-A"); tailOrder.setCurrentStatus("CANCELLED"); - when(transfers.lockNo("TENANT-A", "WTR-1")).thenReturn(transfer); - when(transfers.findNo("TENANT-A", "WTR-1")).thenReturn(transfer); - when(orders.selectById(31L)).thenReturn(tailOrder); - when(transferItems.selectList(any())).thenReturn(List.of()); - when(sourceOrders.selectList(any())).thenReturn(List.of()); - - var result = service.updateDraft("TENANT-A", "WTR-1", "USER-1", request(false)); - - assertEquals(WarehouseTransferService.REJECTED, result.status()); - assertTrue(tailOrder.getSubOrderStatus().startsWith("WAREHOUSE_TRANSFER:REJECTED")); - verify(transfers).updateById(transfer); - verify(orders).updateById(tailOrder); - } - - @Test - void merchantResubmitsRejectedTransferForReview() { - WarehouseTransferOrder transfer = persistedTransfer(); - transfer.setStatus(WarehouseTransferService.REJECTED); - transfer.setInboundCode("WTR-1"); - LastlegOrder tailOrder = historicalOrder(); - tailOrder.setId(31L); tailOrder.setCustomerId("TENANT-A"); tailOrder.setCurrentStatus("CANCELLED"); - tailOrder.setRelabelStatus("NOT_REQUIRED"); - LastlegInboundOrder inbound = new LastlegInboundOrder(); - inbound.setId(101L); inbound.setCustomerId("TENANT-A"); inbound.setStatus(WarehouseTransferService.PENDING_REVIEW); - when(transfers.lockNo("TENANT-A", "WTR-1")).thenReturn(transfer); - when(transfers.findNo("TENANT-A", "WTR-1")).thenReturn(transfer); - when(orders.selectById(31L)).thenReturn(tailOrder); - when(inboundOrders.selectById(101L)).thenReturn(inbound); - when(transferItems.selectList(any())).thenReturn(List.of()); - when(sourceOrders.selectList(any())).thenReturn(List.of()); - - var result = service.submit("TENANT-A", "WTR-1", "USER-1"); - - assertEquals(WarehouseTransferService.PENDING_REVIEW, result.status()); - assertEquals(WarehouseTransferService.PENDING_REVIEW, tailOrder.getCurrentStatus()); - var history = ArgumentCaptor.forClass(com.baiyue.lastleg.entity.LastlegOrderStatusHistory.class); - verify(orderHistories).insert(history.capture()); - assertEquals(WarehouseTransferService.REJECTED, history.getValue().getFromStatus()); - assertEquals(WarehouseTransferService.PENDING_REVIEW, history.getValue().getToStatus()); - } - @Test void merchantCannotUpdateSubmittedTransfer() { WarehouseTransferOrder transfer = persistedTransfer(); diff --git a/scripts/db/incremental.sql b/scripts/db/incremental.sql index 5b027ece5..74105200f 100644 --- a/scripts/db/incremental.sql +++ b/scripts/db/incremental.sql @@ -1369,7 +1369,7 @@ USE baiyue_lastleg; CREATE TABLE IF NOT EXISTS ll_cargo_discard_application ( id BIGINT NOT NULL COMMENT '主键ID', customer_id VARCHAR(128) NOT NULL COMMENT '客户租户ID', - discard_no VARCHAR(64) NOT NULL COMMENT '废弃申请单号', outbound_no VARCHAR(64) NULL COMMENT '废弃出库单号', overseas_warehouse_id VARCHAR(128) NOT NULL COMMENT '海外仓ID', + discard_no VARCHAR(64) NOT NULL COMMENT '废弃申请单号', overseas_warehouse_id VARCHAR(128) NOT NULL COMMENT '海外仓ID', overseas_warehouse_name VARCHAR(255) NULL COMMENT '海外仓名称快照', inbound_codes VARCHAR(2000) NOT NULL COMMENT '入仓单号集合', total_packages INT NOT NULL COMMENT '总箱数', fee_per_package DECIMAL(18,2) NOT NULL COMMENT '每箱处理费用', total_fee DECIMAL(18,2) NOT NULL COMMENT '费用合计', currency CHAR(3) NOT NULL DEFAULT 'USD' COMMENT '币种', @@ -1493,11 +1493,7 @@ PREPARE transfer_inventory_024 FROM @ddl; EXECUTE transfer_inventory_024; DEALLO SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_warehouse_transfer_order' AND column_name='outbound_request_hash'), 'SELECT 1', 'ALTER TABLE ll_warehouse_transfer_order ADD COLUMN outbound_request_hash CHAR(64) NULL COMMENT ''出库请求哈希'' AFTER outbound_idempotency_key')); PREPARE transfer_inventory_024 FROM @ddl; EXECUTE transfer_inventory_024; DEALLOCATE PREPARE transfer_inventory_024; SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_warehouse_transfer_order' AND column_name='outbound_at'), 'SELECT 1', 'ALTER TABLE ll_warehouse_transfer_order ADD COLUMN outbound_at DATETIME(3) NULL COMMENT ''出库时间'' AFTER outbound_request_hash')); -PREPARE transfer_outbound_at_stmt FROM @ddl; EXECUTE transfer_outbound_at_stmt; DEALLOCATE PREPARE transfer_outbound_at_stmt; -SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_warehouse_transfer_order' AND column_name='measured_at'), 'SELECT 1', 'ALTER TABLE ll_warehouse_transfer_order ADD COLUMN measured_at DATETIME(3) NULL COMMENT ''PDA测量完成时间'' AFTER outbound_at')); -PREPARE transfer_measured_at_stmt FROM @ddl; EXECUTE transfer_measured_at_stmt; DEALLOCATE PREPARE transfer_measured_at_stmt; -SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_warehouse_transfer_order' AND column_name='actual_ship_at'), 'SELECT 1', 'ALTER TABLE ll_warehouse_transfer_order ADD COLUMN actual_ship_at DATETIME(3) NULL COMMENT ''实际发货时间'' AFTER measured_at')); -PREPARE transfer_actual_ship_at_stmt FROM @ddl; EXECUTE transfer_actual_ship_at_stmt; DEALLOCATE PREPARE transfer_actual_ship_at_stmt; +PREPARE transfer_inventory_024 FROM @ddl; EXECUTE transfer_inventory_024; DEALLOCATE PREPARE transfer_inventory_024; CREATE TABLE IF NOT EXISTS ll_warehouse_transfer_inventory_allocation ( id BIGINT NOT NULL COMMENT '雪花主键', transfer_order_id BIGINT NOT NULL COMMENT '转仓单ID', customer_id VARCHAR(128) NOT NULL COMMENT '客户租户ID', @@ -4308,5 +4304,3 @@ SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE tab PREPARE dispatch_postal_stmt FROM @ddl; EXECUTE dispatch_postal_stmt; DEALLOCATE PREPARE dispatch_postal_stmt; SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_delivery_order' AND column_name='postal_code'), 'SELECT 1', 'ALTER TABLE ll_delivery_order ADD COLUMN postal_code VARCHAR(32) NULL COMMENT ''私人仓邮编'' AFTER delivery_address')); PREPARE delivery_postal_stmt FROM @ddl; EXECUTE delivery_postal_stmt; DEALLOCATE PREPARE delivery_postal_stmt; -SET @ddl = IF(NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_cargo_discard_application' AND column_name='outbound_no'), 'ALTER TABLE ll_cargo_discard_application ADD COLUMN outbound_no VARCHAR(64) NULL COMMENT ''废弃出库单号'' AFTER discard_no', 'SELECT 1'); -PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;