feat(firstleg): align simulated containers with channel groups
This commit is contained in:
+64
@@ -0,0 +1,64 @@
|
||||
package com.baiyue.firstleg.domain;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/** Stable planning groups used by simulated containers and smart-loading filters. */
|
||||
public final class SmartLoadingChannelGroupCatalog {
|
||||
private SmartLoadingChannelGroupCatalog() {}
|
||||
|
||||
public static final List<Definition> GROUPS = List.of(
|
||||
group("MATSON_CLX", "美森正班", "MATSON_CLX_TRUCK", "MATSON_CLX_PARCEL", "ZBYHK", "ZBYHWK", "ZBYHP"),
|
||||
group("MATSON_MAX", "美森加班", "MATSON_MAX_TRUCK", "MATSON_MAX_PARCEL", "BYHK", "BYHWK", "BYHP"),
|
||||
group("EXX", "EXX", "EXX_TRUCK", "EXX_RAIL_NY", "EXXHK", "EXXHWK", "NYEXX"),
|
||||
group("HEDE", "合德", "HEDE_TRUCK", "HEDE_PARCEL", "HDHK", "HDHWK", "HDHP"),
|
||||
group("OA_LA", "洛杉矶普船", "OA_LA_TRUCK", "OA_LA_FULL_TRUCK", "OA_PARCEL", "PCHK", "PCHWK", "PCKK", "PCHP"),
|
||||
group("OA_CHICAGO", "芝加哥普船", "OA_CHICAGO_RAIL", "CPHT"),
|
||||
group("OA_HOUSTON", "休斯顿普船", "OA_HOUSTON_TRUCK", "HPHK"),
|
||||
group("OA_SAVANNAH", "萨凡纳普船", "OA_SAVANNAH_TRUCK", "SPHK"),
|
||||
group("OA_NEW_YORK_TRUCK", "纽约普船海卡", "OA_NEW_YORK_TRUCK", "NPHK", "NPHWK")
|
||||
);
|
||||
|
||||
private static final Map<String, Definition> BY_CODE = indexByCode();
|
||||
private static final Map<String, Definition> BY_CHANNEL = indexByChannel();
|
||||
|
||||
public static Definition require(String value) {
|
||||
Definition result = BY_CODE.get(normalize(value));
|
||||
if (result == null) throw new IllegalArgumentException("不支持的渠道分组: " + value);
|
||||
return result;
|
||||
}
|
||||
|
||||
public static Definition resolveChannel(String channelCode) {
|
||||
return BY_CHANNEL.get(normalize(channelCode));
|
||||
}
|
||||
|
||||
public static boolean contains(String groupCode, String channelCode) {
|
||||
Definition group = BY_CODE.get(normalize(groupCode));
|
||||
return group != null && group.channelCodes().contains(normalize(channelCode));
|
||||
}
|
||||
|
||||
private static Definition group(String code, String name, String... channelCodes) {
|
||||
return new Definition(code, name, Set.of(channelCodes));
|
||||
}
|
||||
|
||||
private static String normalize(String value) {
|
||||
return value == null ? "" : value.trim().toUpperCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
private static Map<String, Definition> indexByCode() {
|
||||
Map<String, Definition> result = new LinkedHashMap<>();
|
||||
GROUPS.forEach(group -> result.put(group.code(), group));
|
||||
return Map.copyOf(result);
|
||||
}
|
||||
|
||||
private static Map<String, Definition> indexByChannel() {
|
||||
Map<String, Definition> result = new LinkedHashMap<>();
|
||||
GROUPS.forEach(group -> group.channelCodes().forEach(channel -> result.put(channel, group)));
|
||||
return Map.copyOf(result);
|
||||
}
|
||||
|
||||
public record Definition(String code, String name, Set<String> channelCodes) {}
|
||||
}
|
||||
@@ -83,7 +83,7 @@ public final class ContainerDtos {
|
||||
) {}
|
||||
|
||||
@Schema(name = "ContainerSimulationCreateRequest", description = "新增内部模拟柜;内部柜号由服务端生成")
|
||||
public record SimulationCreateRequest(@NotBlank @Size(max = 64) String channelCode,
|
||||
public record SimulationCreateRequest(@NotBlank @Size(max = 64) String channelGroupCode,
|
||||
@NotNull @Positive Long warehouseId, @NotBlank @Size(max = 32) String containerType,
|
||||
@NotNull @DecimalMin(value = "0", inclusive = false) BigDecimal capacityCbm,
|
||||
@NotNull @DecimalMin(value = "0", inclusive = false) BigDecimal maxWeightKg) {}
|
||||
|
||||
@@ -49,7 +49,7 @@ public final class SmartLoadingDtos { private SmartLoadingDtos(){}
|
||||
}
|
||||
}
|
||||
public record WorkbenchContainer(Long bookingId, String bookingNo, String cabinetNo, String containerNo,
|
||||
Long warehouseId, String warehouseName, String channelCode, String channelName, String containerType,
|
||||
Long warehouseId, String warehouseName, String channelGroupCode, String channelCode, String channelName, String containerType,
|
||||
BigDecimal capacityCbm, BigDecimal maxWeightKg,
|
||||
Long planId, String planNo, String planStatus, Integer version) {}
|
||||
public record Candidate(Long accountId, Long inboundId, Long inboundDetailId, Long orderId, String orderNo, String inboundCode,
|
||||
|
||||
@@ -24,6 +24,7 @@ public class ContainerBookingEntity {
|
||||
private String sourceType;
|
||||
private String recordStatus;
|
||||
private Long mergedIntoContainerBookingId;
|
||||
private String channelGroupCode;
|
||||
private String channelCode;
|
||||
private String channelName;
|
||||
private Long oceanBookingId;
|
||||
|
||||
@@ -321,7 +321,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.ba
|
||||
@Select("""
|
||||
<script>
|
||||
SELECT b.id bookingId,b.booking_no bookingNo,b.cabinet_no cabinetNo,b.container_no containerNo,
|
||||
b.warehouse_id warehouseId,b.warehouse_name warehouseName,b.channel_code channelCode,
|
||||
b.warehouse_id warehouseId,b.warehouse_name warehouseName,b.channel_group_code channelGroupCode,b.channel_code channelCode,
|
||||
b.channel_name channelName,b.container_type containerType,b.capacity_cbm capacityCbm,
|
||||
b.max_weight_kg maxWeightKg,p.id planId,p.plan_no planNo,
|
||||
CASE WHEN p.id IS NULL OR NOT EXISTS (SELECT 1 FROM fl_loading_plan_item state_item
|
||||
@@ -331,8 +331,8 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.ba
|
||||
LEFT JOIN fl_loading_plan p ON p.container_booking_id=b.id
|
||||
WHERE b.record_status='ACTIVE'
|
||||
AND (p.id IS NULL OR p.plan_status IN ('EDITING','LOADING'))
|
||||
AND b.channel_code IN
|
||||
<foreach collection="actualChannels" item="channel" open="(" separator="," close=")">#{channel}</foreach>
|
||||
AND b.channel_group_code IN
|
||||
<foreach collection="channelGroups" item="channelGroup" open="(" separator="," close=")">#{channelGroup}</foreach>
|
||||
<if test="query.warehouseId != null">AND b.warehouse_id=#{query.warehouseId}</if>
|
||||
<if test="query.customerId != null or query.inboundCode != null or query.createdStart != null or query.createdEnd != null">
|
||||
AND EXISTS (
|
||||
@@ -351,7 +351,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.ba
|
||||
""")
|
||||
Page<WorkbenchContainer> workbenchContainers(Page<WorkbenchContainer> page,
|
||||
@Param("query") WorkbenchContainerQuery query,
|
||||
@Param("actualChannels") Set<String> actualChannels);
|
||||
@Param("channelGroups") Set<String> channelGroups);
|
||||
|
||||
@Select("""
|
||||
SELECT o.tenant_id accountId,o.id inboundId,i.inbound_detail_id inboundDetailId,
|
||||
|
||||
@@ -29,7 +29,7 @@ public final class OceanBookingDtos {
|
||||
}
|
||||
|
||||
public record LinkCandidateQuery(@NotNull @Positive Long warehouseId,
|
||||
@NotBlank @Size(max = 64) String channelCode,
|
||||
@NotBlank @Size(max = 64) String channelGroupCode,
|
||||
@NotBlank @Size(max = 32) String containerType,
|
||||
@Min(1) Long page,
|
||||
@Min(1) @Max(200) Long size) {
|
||||
|
||||
+6
-2
@@ -54,6 +54,7 @@ public interface OceanBookingMapper extends BaseMapper<OceanBookingEntity> {
|
||||
Page<OceanBookingEntity> selectPage(Page<OceanBookingEntity> page, @Param("query") Query query);
|
||||
|
||||
@Select("""
|
||||
<script>
|
||||
SELECT booking.id ocean_booking_id,booking.booking_no,requirement.id requirement_id,
|
||||
requirement.container_type,requirement.capacity_cbm,requirement.max_weight_kg,
|
||||
requirement.quantity required_quantity,
|
||||
@@ -75,13 +76,16 @@ public interface OceanBookingMapper extends BaseMapper<OceanBookingEntity> {
|
||||
AND allocated.requirement_id=requirement.id
|
||||
WHERE booking.status='RELEASED'
|
||||
AND booking.warehouse_id=#{query.warehouseId}
|
||||
AND booking.channel_code=#{query.channelCode}
|
||||
AND booking.channel_code IN
|
||||
<foreach collection="channelCodes" item="channelCode" open="(" separator="," close=")">#{channelCode}</foreach>
|
||||
AND requirement.container_type=#{query.containerType}
|
||||
AND requirement.quantity>COALESCE(allocated.allocated_quantity,0)
|
||||
ORDER BY booking.updated_at DESC,booking.id DESC,requirement.sort_no ASC,requirement.id ASC
|
||||
</script>
|
||||
""")
|
||||
Page<LinkCandidate> selectLinkCandidates(Page<LinkCandidate> page,
|
||||
@Param("query") LinkCandidateQuery query);
|
||||
@Param("query") LinkCandidateQuery query,
|
||||
@Param("channelCodes") java.util.Set<String> channelCodes);
|
||||
|
||||
@Update("""
|
||||
UPDATE fl_ocean_booking SET status=#{toStatus},
|
||||
|
||||
+2
-2
@@ -33,8 +33,8 @@ public class OceanBookingRepository {
|
||||
public PageResult<OceanBookingEntity> page(Query query) {
|
||||
return PageResult.from(bookings.selectPage(new Page<>(query.page(), query.size()), query));
|
||||
}
|
||||
public PageResult<LinkCandidate> linkCandidates(LinkCandidateQuery query) {
|
||||
return PageResult.from(bookings.selectLinkCandidates(new Page<>(query.page(), query.size()), query));
|
||||
public PageResult<LinkCandidate> linkCandidates(LinkCandidateQuery query, java.util.Set<String> channelCodes) {
|
||||
return PageResult.from(bookings.selectLinkCandidates(new Page<>(query.page(), query.size()), query, channelCodes));
|
||||
}
|
||||
public OceanBookingEntity find(long id) { return bookings.selectById(id); }
|
||||
public OceanBookingEntity findForUpdate(long id) { return bookings.selectForUpdate(id); }
|
||||
|
||||
+5
-1
@@ -10,6 +10,7 @@ import com.baiyue.firstleg.client.WarehousePartnerClient;
|
||||
import com.baiyue.firstleg.client.WarehouseSupplierClient;
|
||||
import com.baiyue.firstleg.oceanbooking.domain.OceanBookingStatus;
|
||||
import com.baiyue.firstleg.domain.ContainerSpecificationCatalog;
|
||||
import com.baiyue.firstleg.domain.SmartLoadingChannelGroupCatalog;
|
||||
import com.baiyue.firstleg.oceanbooking.dto.OceanBookingDtos.*;
|
||||
import com.baiyue.firstleg.oceanbooking.entity.*;
|
||||
import com.baiyue.firstleg.oceanbooking.repository.OceanBookingRepository;
|
||||
@@ -71,8 +72,11 @@ public class OceanBookingService {
|
||||
}
|
||||
|
||||
public PageResult<LinkCandidate> linkCandidates(LinkCandidateQuery query) {
|
||||
SmartLoadingChannelGroupCatalog.Definition group;
|
||||
try { group = SmartLoadingChannelGroupCatalog.require(query.channelGroupCode()); }
|
||||
catch (IllegalArgumentException exception) { throw badRequest(exception.getMessage()); }
|
||||
return repository.linkCandidates(new LinkCandidateQuery(query.warehouseId(),
|
||||
query.channelCode().trim(), query.containerType().trim(), query.page(), query.size()));
|
||||
group.code(), query.containerType().trim(), query.page(), query.size()), group.channelCodes());
|
||||
}
|
||||
|
||||
public List<ChannelOption> channels(long originWarehouseId) {
|
||||
|
||||
@@ -49,9 +49,9 @@ public class SmartLoadingRepository {
|
||||
return units.isEmpty() ? List.of() : candidates.globalCandidatesByUnits(units);
|
||||
}
|
||||
public PageResult<WorkbenchContainer> workbenchContainers(WorkbenchContainerQuery query,
|
||||
Set<String> actualChannels) {
|
||||
Set<String> channelGroups) {
|
||||
return PageResult.from(candidates.workbenchContainers(
|
||||
new Page<>(query.page(), query.size()), query, actualChannels));
|
||||
new Page<>(query.page(), query.size()), query, channelGroups));
|
||||
}
|
||||
public List<ChannelContainerCountRow> channelContainerCounts(long warehouseId) {
|
||||
return candidates.channelContainerCounts(warehouseId);
|
||||
|
||||
@@ -7,6 +7,7 @@ import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.firstleg.client.WarehouseMasterDataClient;
|
||||
import com.baiyue.firstleg.domain.SmartLoadingStatus;
|
||||
import com.baiyue.firstleg.domain.ContainerSpecificationCatalog;
|
||||
import com.baiyue.firstleg.domain.SmartLoadingChannelGroupCatalog;
|
||||
import com.baiyue.firstleg.dto.ContainerDtos.FullUpdateRequest;
|
||||
import com.baiyue.firstleg.dto.ContainerDtos.SaveRequest;
|
||||
import com.baiyue.firstleg.dto.ContainerDtos.SimulationCreateRequest;
|
||||
@@ -55,13 +56,16 @@ public class ContainerService {
|
||||
WarehouseMasterDataClient.Warehouse warehouse = requireDomesticTransitWarehouse(request.warehouseId());
|
||||
Long versionId = priceBooks.activeVersionId();
|
||||
if (versionId == null) throw conflict("当前没有 ACTIVE 价格版本");
|
||||
ChannelRow channel = priceBooks.channels(versionId, warehouse.id()).stream()
|
||||
.filter(row -> request.channelCode().trim().equals(row.getChannelCode())).findFirst()
|
||||
.orElseThrow(() -> conflict("所选物流渠道已失效,请重新选择"));
|
||||
SmartLoadingChannelGroupCatalog.Definition channelGroup;
|
||||
try { channelGroup = SmartLoadingChannelGroupCatalog.require(request.channelGroupCode()); }
|
||||
catch (IllegalArgumentException exception) { throw conflict(exception.getMessage()); }
|
||||
boolean activeGroup = priceBooks.channels(versionId, warehouse.id()).stream()
|
||||
.map(ChannelRow::getChannelCode).anyMatch(channelGroup.channelCodes()::contains);
|
||||
if (!activeGroup) throw conflict("所选物流渠道分组在当前仓库已失效,请重新选择");
|
||||
ContainerBookingEntity value = new ContainerBookingEntity(); value.setId(IdWorker.getId());
|
||||
value.setBookingNo(documentNumbers.next("CB")); value.setCabinetNo(documentNumbers.next("CAB"));
|
||||
value.setSourceType("SIMULATION"); value.setRecordStatus("ACTIVE");
|
||||
value.setChannelCode(channel.getChannelCode()); value.setChannelName(channel.getChannelName());
|
||||
value.setChannelGroupCode(channelGroup.code()); value.setChannelCode(null); value.setChannelName(null);
|
||||
value.setWarehouseId(warehouse.id()); value.setWarehouseName(warehouse.warehouseName());
|
||||
value.setContainerType(specification.containerType()); value.setCapacityCbm(specification.capacityCbm());
|
||||
value.setMaxWeightKg(specification.maxWeightKg()); value.setCreatedBy(operatorId); value.setCreatedByName(operatorName);
|
||||
@@ -85,9 +89,9 @@ public class ContainerService {
|
||||
String containerNo = requestedContainerNo == null ? null : Iso6346ContainerNumber.require(requestedContainerNo);
|
||||
if (containerNo != null && repository.containerNoExists(containerNo, id)) throw conflict("集装箱号已存在");
|
||||
if (!java.util.Objects.equals(container.getWarehouseId(), booking.getWarehouseId())
|
||||
|| !java.util.Objects.equals(container.getChannelCode(), booking.getChannelCode())
|
||||
|| !SmartLoadingChannelGroupCatalog.contains(container.getChannelGroupCode(), booking.getChannelCode())
|
||||
|| !java.util.Objects.equals(container.getContainerType(), requirement.getContainerType())) {
|
||||
throw conflict("模拟柜与真实订舱柜仓库、渠道或柜型不一致");
|
||||
throw conflict("模拟柜与真实订舱柜仓库、渠道分组或柜型不一致");
|
||||
}
|
||||
try {
|
||||
if (repository.linkReleasedBooking(id, request.oceanBookingId(), request.requirementId(), containerNo,
|
||||
|
||||
@@ -6,6 +6,7 @@ import com.baiyue.common.core.api.PageResult;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.common.audit.BusinessAudit;
|
||||
import com.baiyue.firstleg.dto.SmartLoadingDtos.*;
|
||||
import com.baiyue.firstleg.domain.SmartLoadingChannelGroupCatalog;
|
||||
import com.baiyue.firstleg.client.OrderCargoClient;
|
||||
import com.baiyue.firstleg.client.PendingReviewOrderClient;
|
||||
import com.baiyue.firstleg.entity.*;
|
||||
@@ -42,17 +43,6 @@ import java.util.*;
|
||||
@Service
|
||||
public class SmartLoadingService {
|
||||
private static final BigDecimal CAPACITY_LIMIT_RATIO = new BigDecimal("1.50");
|
||||
private static final Map<String, Set<String>> WORKBENCH_CHANNEL_GROUPS = Map.of(
|
||||
"MATSON_CLX", Set.of("MATSON_CLX_TRUCK", "MATSON_CLX_PARCEL"),
|
||||
"MATSON_MAX", Set.of("MATSON_MAX_TRUCK", "MATSON_MAX_PARCEL"),
|
||||
"EXX", Set.of("EXX_TRUCK", "EXX_RAIL_NY"),
|
||||
"HEDE", Set.of("HEDE_TRUCK", "HEDE_PARCEL"),
|
||||
"OA_LA", Set.of("OA_LA_TRUCK", "OA_LA_FULL_TRUCK", "OA_PARCEL"),
|
||||
"OA_CHICAGO", Set.of("OA_CHICAGO_RAIL"),
|
||||
"OA_HOUSTON", Set.of("OA_HOUSTON_TRUCK"),
|
||||
"OA_SAVANNAH", Set.of("OA_SAVANNAH_TRUCK"),
|
||||
"OA_NEW_YORK_TRUCK", Set.of("OA_NEW_YORK_TRUCK")
|
||||
);
|
||||
@Resource private SmartLoadingRepository repository;
|
||||
@Resource private com.baiyue.firstleg.mapper.DropshipPalletizationMapper dropshipPallets;
|
||||
@Resource private OrderCargoClient orderCargoClient;
|
||||
@@ -102,7 +92,7 @@ public class SmartLoadingService {
|
||||
public List<Candidate> candidates(long bookingId, long warehouseId) {
|
||||
ContainerBookingEntity booking = requireBooking(bookingId);
|
||||
if (!Objects.equals(booking.getWarehouseId(), warehouseId)) throw validation("柜次仓库与候选仓库不一致");
|
||||
if (!StringUtils.hasText(booking.getChannelCode())) throw validation("柜次缺少渠道快照,请先完成存量数据回填");
|
||||
if (!StringUtils.hasText(booking.getChannelGroupCode())) throw validation("柜次缺少计划渠道,请重新创建模拟柜");
|
||||
List<Candidate> values = candidates(warehouseId).stream()
|
||||
.filter(candidate -> sameChannel(booking, candidate) && sameCustomerScope(booking, candidate)).toList();
|
||||
return enrichColoadCandidates(booking, repository.planByBooking(bookingId), values);
|
||||
@@ -266,7 +256,7 @@ public class SmartLoadingService {
|
||||
&& query.createdStart().isAfter(query.createdEnd())) {
|
||||
throw validation("创建开始日期不能晚于结束日期");
|
||||
}
|
||||
return repository.workbenchContainers(query, actualChannels(query.channelGroupCodes()));
|
||||
return repository.workbenchContainers(query, normalizedGroups(query.channelGroupCodes()));
|
||||
}
|
||||
|
||||
private List<InboundCandidate> inboundCandidates(List<Candidate> details) {
|
||||
@@ -384,13 +374,17 @@ public class SmartLoadingService {
|
||||
private static Set<String> actualChannels(List<String> groups) {
|
||||
Set<String> actual = new LinkedHashSet<>();
|
||||
for (String value : groups) {
|
||||
String normalized = value == null ? "" : value.trim().toUpperCase(Locale.ROOT);
|
||||
Set<String> mapped = WORKBENCH_CHANNEL_GROUPS.get(normalized);
|
||||
if (mapped == null) throw validation("不支持的渠道分组: " + value);
|
||||
actual.addAll(mapped);
|
||||
try { actual.addAll(SmartLoadingChannelGroupCatalog.require(value).channelCodes()); }
|
||||
catch (IllegalArgumentException exception) { throw validation(exception.getMessage()); }
|
||||
}
|
||||
return actual;
|
||||
}
|
||||
private static Set<String> normalizedGroups(List<String> groups) {
|
||||
return groups.stream().map(value -> {
|
||||
try { return SmartLoadingChannelGroupCatalog.require(value).code(); }
|
||||
catch (IllegalArgumentException exception) { throw validation(exception.getMessage()); }
|
||||
}).collect(java.util.stream.Collectors.toCollection(LinkedHashSet::new));
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public Plan draftByBooking(long bookingId) {
|
||||
@@ -1689,8 +1683,10 @@ public class SmartLoadingService {
|
||||
}
|
||||
private ContainerBookingEntity requireBooking(long id){ContainerBookingEntity b=repository.booking(id);if(b==null)throw new BizException(ResultCode.NOT_FOUND,"货柜预订不存在");return b;}
|
||||
private static boolean sameChannel(ContainerBookingEntity booking, Candidate candidate) {
|
||||
return StringUtils.hasText(booking.getChannelCode()) && StringUtils.hasText(candidate.channelCode())
|
||||
&& booking.getChannelCode().equals(candidate.channelCode());
|
||||
SmartLoadingChannelGroupCatalog.Definition candidateGroup =
|
||||
SmartLoadingChannelGroupCatalog.resolveChannel(candidate.channelCode());
|
||||
return StringUtils.hasText(booking.getChannelGroupCode()) && candidateGroup != null
|
||||
&& booking.getChannelGroupCode().equals(candidateGroup.code());
|
||||
}
|
||||
private static boolean sameCustomerScope(ContainerBookingEntity booking, Candidate candidate) {
|
||||
return booking.getCustomerId() == null || Objects.equals(booking.getCustomerId(), candidate.accountId());
|
||||
|
||||
Reference in New Issue
Block a user