From ed36c43af4ea29db45bc34c25f4d5aad76c04abc Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Tue, 29 Sep 2026 04:17:44 +0100 Subject: [PATCH] fix(firstleg): source sailing channels from active price book --- .../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 +++++++++++++------ 7 files changed, 78 insertions(+), 27 deletions(-) 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) {