fix(firstleg): align coload group eligibility
This commit is contained in:
@@ -16417,8 +16417,7 @@ CREATE TABLE IF NOT EXISTS fl_coload_group (
|
|||||||
id BIGINT UNSIGNED NOT NULL COMMENT '同柜不可拆组ID',group_no VARCHAR(40) NOT NULL COMMENT '稳定组号CLG-{id}',
|
id BIGINT UNSIGNED NOT NULL COMMENT '同柜不可拆组ID',group_no VARCHAR(40) NOT NULL COMMENT '稳定组号CLG-{id}',
|
||||||
group_status VARCHAR(16) NOT NULL COMMENT '组状态:ACTIVE、LOCKED、CONSUMED、CANCELLED',
|
group_status VARCHAR(16) NOT NULL COMMENT '组状态:ACTIVE、LOCKED、CONSUMED、CANCELLED',
|
||||||
warehouse_id BIGINT UNSIGNED NOT NULL COMMENT '国内收货仓ID快照',channel_code VARCHAR(64) NOT NULL COMMENT '物流渠道编码快照',
|
warehouse_id BIGINT UNSIGNED NOT NULL COMMENT '国内收货仓ID快照',channel_code VARCHAR(64) NOT NULL COMMENT '物流渠道编码快照',
|
||||||
destination_type VARCHAR(32) NOT NULL COMMENT '目的类型快照',destination_warehouse_id BIGINT UNSIGNED NULL COMMENT '目的仓ID快照',
|
locked_plan_id BIGINT UNSIGNED NULL COMMENT 'LOCKED/CONSUMED绑定装柜计划ID',
|
||||||
destination_code VARCHAR(64) NULL COMMENT '目的仓或目的港编码快照',locked_plan_id BIGINT UNSIGNED NULL COMMENT 'LOCKED/CONSUMED绑定装柜计划ID',
|
|
||||||
version INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '乐观版本',created_by BIGINT UNSIGNED NOT NULL COMMENT '创建人ID',created_by_name VARCHAR(64) NULL COMMENT '创建人名称快照',
|
version INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '乐观版本',created_by BIGINT UNSIGNED NOT NULL COMMENT '创建人ID',created_by_name VARCHAR(64) 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 '更新时间',
|
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 '更新时间',
|
||||||
locked_by BIGINT UNSIGNED NULL COMMENT '锁定操作人ID',locked_by_name VARCHAR(64) NULL COMMENT '锁定操作人名称快照',locked_at DATETIME(3) NULL COMMENT '锁定时间',
|
locked_by BIGINT UNSIGNED NULL COMMENT '锁定操作人ID',locked_by_name VARCHAR(64) NULL COMMENT '锁定操作人名称快照',locked_at DATETIME(3) NULL COMMENT '锁定时间',
|
||||||
@@ -16428,7 +16427,6 @@ CREATE TABLE IF NOT EXISTS fl_coload_group (
|
|||||||
KEY idx_coload_group_plan(locked_plan_id,group_status),
|
KEY idx_coload_group_plan(locked_plan_id,group_status),
|
||||||
CONSTRAINT fk_coload_group_plan FOREIGN KEY(locked_plan_id) REFERENCES fl_loading_plan(id),
|
CONSTRAINT fk_coload_group_plan FOREIGN KEY(locked_plan_id) REFERENCES fl_loading_plan(id),
|
||||||
CONSTRAINT chk_coload_group_status CHECK(group_status IN('ACTIVE','LOCKED','CONSUMED','CANCELLED')),
|
CONSTRAINT chk_coload_group_status CHECK(group_status IN('ACTIVE','LOCKED','CONSUMED','CANCELLED')),
|
||||||
CONSTRAINT chk_coload_group_destination CHECK(destination_warehouse_id IS NOT NULL OR destination_code IS NOT NULL),
|
|
||||||
CONSTRAINT chk_coload_group_lock CHECK((group_status IN('ACTIVE','CANCELLED') AND locked_plan_id IS NULL)
|
CONSTRAINT chk_coload_group_lock CHECK((group_status IN('ACTIVE','CANCELLED') AND locked_plan_id IS NULL)
|
||||||
OR(group_status IN('LOCKED','CONSUMED') AND locked_plan_id IS NOT NULL))
|
OR(group_status IN('LOCKED','CONSUMED') AND locked_plan_id IS NOT NULL))
|
||||||
)ENGINE=InnoDB COMMENT='同柜且不可拆业务组聚合根';
|
)ENGINE=InnoDB COMMENT='同柜且不可拆业务组聚合根';
|
||||||
|
|||||||
+1
-1
@@ -33,7 +33,7 @@ import java.util.List;
|
|||||||
@Validated
|
@Validated
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/platform/v1/first-leg/coload-groups")
|
@RequestMapping("/api/platform/v1/first-leg/coload-groups")
|
||||||
@Tag(name = "平台同柜不可拆组", description = "跨客户、同仓同渠道同目的仓港的整组装柜约束")
|
@Tag(name = "平台同柜不可拆组", description = "跨客户、同国内仓同渠道的整组装柜约束")
|
||||||
public class PlatformColoadGroupController {
|
public class PlatformColoadGroupController {
|
||||||
@Resource
|
@Resource
|
||||||
private ColoadGroupService service;
|
private ColoadGroupService service;
|
||||||
|
|||||||
+2
-8
@@ -43,9 +43,6 @@ public final class ColoadGroupModels {
|
|||||||
String status,
|
String status,
|
||||||
long warehouseId,
|
long warehouseId,
|
||||||
String channelCode,
|
String channelCode,
|
||||||
String destinationType,
|
|
||||||
Long destinationWarehouseId,
|
|
||||||
String destinationCode,
|
|
||||||
Long lockedPlanId,
|
Long lockedPlanId,
|
||||||
int version,
|
int version,
|
||||||
List<Member> members,
|
List<Member> members,
|
||||||
@@ -60,11 +57,9 @@ public final class ColoadGroupModels {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public GroupSnapshot(long groupId, String groupNo, String status, long warehouseId,
|
public GroupSnapshot(long groupId, String groupNo, String status, long warehouseId,
|
||||||
String channelCode, String destinationType, Long destinationWarehouseId,
|
String channelCode, Long lockedPlanId, int version,
|
||||||
String destinationCode, Long lockedPlanId, int version,
|
|
||||||
List<Member> members, LocalDateTime createdAt, LocalDateTime updatedAt) {
|
List<Member> members, LocalDateTime createdAt, LocalDateTime updatedAt) {
|
||||||
this(groupId, groupNo, status, warehouseId, channelCode, destinationType,
|
this(groupId, groupNo, status, warehouseId, channelCode, lockedPlanId, version, members,
|
||||||
destinationWarehouseId, destinationCode, lockedPlanId, version, members,
|
|
||||||
members == null ? 0 : members.stream().mapToInt(Member::fullPackageCount).sum(),
|
members == null ? 0 : members.stream().mapToInt(Member::fullPackageCount).sum(),
|
||||||
createdAt, updatedAt);
|
createdAt, updatedAt);
|
||||||
}
|
}
|
||||||
@@ -72,7 +67,6 @@ public final class ColoadGroupModels {
|
|||||||
|
|
||||||
public record OrderGroupInfo(
|
public record OrderGroupInfo(
|
||||||
long orderId,
|
long orderId,
|
||||||
long inboundId,
|
|
||||||
GroupSnapshot group
|
GroupSnapshot group
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-29
@@ -15,21 +15,11 @@ import static com.baiyue.firstleg.loading.coload.persistence.ColoadPersistenceRo
|
|||||||
public interface ColoadGroupMapper {
|
public interface ColoadGroupMapper {
|
||||||
@Select({"<script>", """
|
@Select({"<script>", """
|
||||||
SELECT o.tenant_id tenantId,o.order_id orderId,o.id inboundId,
|
SELECT o.tenant_id tenantId,o.order_id orderId,o.id inboundId,
|
||||||
o.process_status processStatus,o.inbound_role inboundRole,
|
o.process_status processStatus,o.warehouse_id warehouseId,
|
||||||
o.split_status splitStatus,o.split_purpose splitPurpose,
|
o.channel_code channelCode,o.total_package_count fullPackageCount,
|
||||||
o.parent_inbound_id parentInboundId,o.warehouse_id warehouseId,
|
|
||||||
o.channel_code channelCode,o.destination_type destinationType,
|
|
||||||
o.destination_warehouse_id destinationWarehouseId,
|
|
||||||
o.destination_code destinationCode,o.total_package_count fullPackageCount,
|
|
||||||
(SELECT COUNT(*) FROM fl_loading_plan_item item
|
(SELECT COUNT(*) FROM fl_loading_plan_item item
|
||||||
WHERE item.account_id=o.tenant_id AND item.inbound_id=o.id AND item.deleted=0)
|
WHERE item.account_id=o.tenant_id AND item.inbound_id=o.id AND item.deleted=0)
|
||||||
activeAllocationCount,
|
activeAllocationCount,
|
||||||
(SELECT COUNT(*) FROM fl_inbound_order child
|
|
||||||
WHERE child.tenant_id=o.tenant_id AND child.parent_inbound_id=o.id AND child.deleted=0)
|
|
||||||
childCount,
|
|
||||||
(SELECT COUNT(*) FROM fl_inbound_split_batch batch
|
|
||||||
WHERE batch.tenant_id=o.tenant_id AND batch.parent_inbound_id=o.id
|
|
||||||
AND batch.status IN ('PREPARED','APPLIED')) splitBatchCount,
|
|
||||||
(SELECT member.group_id FROM fl_coload_group_member member
|
(SELECT member.group_id FROM fl_coload_group_member member
|
||||||
WHERE member.tenant_id=o.tenant_id AND member.inbound_id=o.id
|
WHERE member.tenant_id=o.tenant_id AND member.inbound_id=o.id
|
||||||
AND member.active_flag=1 LIMIT 1) activeGroupId
|
AND member.active_flag=1 LIMIT 1) activeGroupId
|
||||||
@@ -41,20 +31,15 @@ public interface ColoadGroupMapper {
|
|||||||
|
|
||||||
@Insert("""
|
@Insert("""
|
||||||
INSERT INTO fl_coload_group
|
INSERT INTO fl_coload_group
|
||||||
(id,group_no,group_status,warehouse_id,channel_code,destination_type,
|
(id,group_no,group_status,warehouse_id,channel_code,locked_plan_id,version,
|
||||||
destination_warehouse_id,destination_code,locked_plan_id,version,
|
|
||||||
created_by,created_by_name,created_at,updated_at)
|
created_by,created_by_name,created_at,updated_at)
|
||||||
VALUES
|
VALUES
|
||||||
(#{id},#{groupNo},'ACTIVE',#{warehouseId},#{channelCode},#{destinationType},
|
(#{id},#{groupNo},'ACTIVE',#{warehouseId},#{channelCode},NULL,0,
|
||||||
#{destinationWarehouseId},#{destinationCode},NULL,0,
|
|
||||||
#{operatorId},#{operatorName},CURRENT_TIMESTAMP(3),CURRENT_TIMESTAMP(3))
|
#{operatorId},#{operatorName},CURRENT_TIMESTAMP(3),CURRENT_TIMESTAMP(3))
|
||||||
""")
|
""")
|
||||||
int insertGroup(@Param("id") long id, @Param("groupNo") String groupNo,
|
int insertGroup(@Param("id") long id, @Param("groupNo") String groupNo,
|
||||||
@Param("warehouseId") long warehouseId,
|
@Param("warehouseId") long warehouseId,
|
||||||
@Param("channelCode") String channelCode,
|
@Param("channelCode") String channelCode,
|
||||||
@Param("destinationType") String destinationType,
|
|
||||||
@Param("destinationWarehouseId") Long destinationWarehouseId,
|
|
||||||
@Param("destinationCode") String destinationCode,
|
|
||||||
@Param("operatorId") long operatorId,
|
@Param("operatorId") long operatorId,
|
||||||
@Param("operatorName") String operatorName);
|
@Param("operatorName") String operatorName);
|
||||||
|
|
||||||
@@ -70,16 +55,14 @@ public interface ColoadGroupMapper {
|
|||||||
@Param("fullPackageCount") int fullPackageCount);
|
@Param("fullPackageCount") int fullPackageCount);
|
||||||
|
|
||||||
@Select("SELECT id,group_no groupNo,group_status groupStatus,warehouse_id warehouseId,"
|
@Select("SELECT id,group_no groupNo,group_status groupStatus,warehouse_id warehouseId,"
|
||||||
+ "channel_code channelCode,destination_type destinationType,"
|
+ "channel_code channelCode,locked_plan_id lockedPlanId,version,"
|
||||||
+ "destination_warehouse_id destinationWarehouseId,destination_code destinationCode,"
|
+ "created_at createdAt,updated_at updatedAt "
|
||||||
+ "locked_plan_id lockedPlanId,version,created_at createdAt,updated_at updatedAt "
|
|
||||||
+ "FROM fl_coload_group WHERE id=#{groupId}")
|
+ "FROM fl_coload_group WHERE id=#{groupId}")
|
||||||
GroupRow group(@Param("groupId") long groupId);
|
GroupRow group(@Param("groupId") long groupId);
|
||||||
|
|
||||||
@Select("SELECT id,group_no groupNo,group_status groupStatus,warehouse_id warehouseId,"
|
@Select("SELECT id,group_no groupNo,group_status groupStatus,warehouse_id warehouseId,"
|
||||||
+ "channel_code channelCode,destination_type destinationType,"
|
+ "channel_code channelCode,locked_plan_id lockedPlanId,version,"
|
||||||
+ "destination_warehouse_id destinationWarehouseId,destination_code destinationCode,"
|
+ "created_at createdAt,updated_at updatedAt "
|
||||||
+ "locked_plan_id lockedPlanId,version,created_at createdAt,updated_at updatedAt "
|
|
||||||
+ "FROM fl_coload_group WHERE id=#{groupId} FOR UPDATE")
|
+ "FROM fl_coload_group WHERE id=#{groupId} FOR UPDATE")
|
||||||
GroupRow lockGroup(@Param("groupId") long groupId);
|
GroupRow lockGroup(@Param("groupId") long groupId);
|
||||||
|
|
||||||
@@ -91,9 +74,7 @@ public interface ColoadGroupMapper {
|
|||||||
@Select("""
|
@Select("""
|
||||||
SELECT group_row.id,group_row.group_no groupNo,group_row.group_status groupStatus,
|
SELECT group_row.id,group_row.group_no groupNo,group_row.group_status groupStatus,
|
||||||
group_row.warehouse_id warehouseId,group_row.channel_code channelCode,
|
group_row.warehouse_id warehouseId,group_row.channel_code channelCode,
|
||||||
group_row.destination_type destinationType,
|
group_row.locked_plan_id lockedPlanId,
|
||||||
group_row.destination_warehouse_id destinationWarehouseId,
|
|
||||||
group_row.destination_code destinationCode,group_row.locked_plan_id lockedPlanId,
|
|
||||||
group_row.version,group_row.created_at createdAt,group_row.updated_at updatedAt
|
group_row.version,group_row.created_at createdAt,group_row.updated_at updatedAt
|
||||||
FROM fl_coload_group_member member
|
FROM fl_coload_group_member member
|
||||||
JOIN fl_coload_group group_row ON group_row.id=member.group_id
|
JOIN fl_coload_group group_row ON group_row.id=member.group_id
|
||||||
@@ -105,7 +86,7 @@ public interface ColoadGroupMapper {
|
|||||||
@Param("inboundId") long inboundId);
|
@Param("inboundId") long inboundId);
|
||||||
|
|
||||||
@Select({"<script>", """
|
@Select({"<script>", """
|
||||||
SELECT member.order_id orderId,member.inbound_id inboundId,member.group_id groupId
|
SELECT member.order_id orderId,member.group_id groupId
|
||||||
FROM fl_coload_group_member member
|
FROM fl_coload_group_member member
|
||||||
JOIN fl_coload_group group_row ON group_row.id=member.group_id
|
JOIN fl_coload_group group_row ON group_row.id=member.group_id
|
||||||
WHERE member.order_id IN
|
WHERE member.order_id IN
|
||||||
|
|||||||
-13
@@ -17,19 +17,10 @@ public final class ColoadPersistenceRows {
|
|||||||
private Long orderId;
|
private Long orderId;
|
||||||
private Long inboundId;
|
private Long inboundId;
|
||||||
private String processStatus;
|
private String processStatus;
|
||||||
private String inboundRole;
|
|
||||||
private String splitStatus;
|
|
||||||
private String splitPurpose;
|
|
||||||
private Long parentInboundId;
|
|
||||||
private Long warehouseId;
|
private Long warehouseId;
|
||||||
private String channelCode;
|
private String channelCode;
|
||||||
private String destinationType;
|
|
||||||
private Long destinationWarehouseId;
|
|
||||||
private String destinationCode;
|
|
||||||
private Integer fullPackageCount;
|
private Integer fullPackageCount;
|
||||||
private Integer activeAllocationCount;
|
private Integer activeAllocationCount;
|
||||||
private Integer childCount;
|
|
||||||
private Integer splitBatchCount;
|
|
||||||
private Long activeGroupId;
|
private Long activeGroupId;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,9 +32,6 @@ public final class ColoadPersistenceRows {
|
|||||||
private String groupStatus;
|
private String groupStatus;
|
||||||
private Long warehouseId;
|
private Long warehouseId;
|
||||||
private String channelCode;
|
private String channelCode;
|
||||||
private String destinationType;
|
|
||||||
private Long destinationWarehouseId;
|
|
||||||
private String destinationCode;
|
|
||||||
private Long lockedPlanId;
|
private Long lockedPlanId;
|
||||||
private Integer version;
|
private Integer version;
|
||||||
private LocalDateTime createdAt;
|
private LocalDateTime createdAt;
|
||||||
@@ -63,7 +51,6 @@ public final class ColoadPersistenceRows {
|
|||||||
@Setter
|
@Setter
|
||||||
public static class OrderGroupLinkRow {
|
public static class OrderGroupLinkRow {
|
||||||
private Long orderId;
|
private Long orderId;
|
||||||
private Long inboundId;
|
|
||||||
private Long groupId;
|
private Long groupId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+18
-35
@@ -52,8 +52,7 @@ public class ColoadGroupService implements ColoadGroupAssignmentPort {
|
|||||||
String groupNo = "CLG-" + groupId;
|
String groupNo = "CLG-" + groupId;
|
||||||
InboundCandidateRow first = candidates.get(0);
|
InboundCandidateRow first = candidates.get(0);
|
||||||
mapper.insertGroup(groupId, groupNo, first.getWarehouseId(), first.getChannelCode().trim(),
|
mapper.insertGroup(groupId, groupNo, first.getWarehouseId(), first.getChannelCode().trim(),
|
||||||
normalized(first.getDestinationType()), first.getDestinationWarehouseId(),
|
operatorId, trim(operatorName));
|
||||||
trim(first.getDestinationCode()), operatorId, trim(operatorName));
|
|
||||||
for (InboundCandidateRow candidate : candidates) {
|
for (InboundCandidateRow candidate : candidates) {
|
||||||
mapper.insertMember(IdWorker.getId(), groupId, candidate.getTenantId(), candidate.getOrderId(),
|
mapper.insertMember(IdWorker.getId(), groupId, candidate.getTenantId(), candidate.getOrderId(),
|
||||||
candidate.getInboundId(), candidate.getFullPackageCount());
|
candidate.getInboundId(), candidate.getFullPackageCount());
|
||||||
@@ -98,7 +97,7 @@ public class ColoadGroupService implements ColoadGroupAssignmentPort {
|
|||||||
if (latest.containsKey(link.getOrderId())) continue;
|
if (latest.containsKey(link.getOrderId())) continue;
|
||||||
GroupSnapshot group = groups.computeIfAbsent(link.getGroupId(), this::requiredSnapshot);
|
GroupSnapshot group = groups.computeIfAbsent(link.getGroupId(), this::requiredSnapshot);
|
||||||
if (!CURRENT_GROUP_STATUSES.contains(group.status())) continue;
|
if (!CURRENT_GROUP_STATUSES.contains(group.status())) continue;
|
||||||
latest.put(link.getOrderId(), new OrderGroupInfo(link.getOrderId(), link.getInboundId(), group));
|
latest.put(link.getOrderId(), new OrderGroupInfo(link.getOrderId(), group));
|
||||||
}
|
}
|
||||||
return orderIds.stream().map(latest::get).filter(Objects::nonNull).toList();
|
return orderIds.stream().map(latest::get).filter(Objects::nonNull).toList();
|
||||||
}
|
}
|
||||||
@@ -188,10 +187,10 @@ public class ColoadGroupService implements ColoadGroupAssignmentPort {
|
|||||||
for (Long orderId : orderIds) {
|
for (Long orderId : orderIds) {
|
||||||
List<InboundCandidateRow> eligible = byOrder.getOrDefault(orderId, List.of()).stream()
|
List<InboundCandidateRow> eligible = byOrder.getOrDefault(orderId, List.of()).stream()
|
||||||
.filter(row -> eligible(row, allowedGroupId)).toList();
|
.filter(row -> eligible(row, allowedGroupId)).toList();
|
||||||
if (eligible.size() != 1) {
|
if (eligible.isEmpty()) {
|
||||||
throw conflict("订单 " + orderId + " 没有唯一可创建同柜不可拆组的当前入库单");
|
throw conflict("订单 " + orderId + " 不存在可创建同柜不可拆组的可用入库片段");
|
||||||
}
|
}
|
||||||
resolved.add(eligible.get(0));
|
resolved.addAll(eligible);
|
||||||
}
|
}
|
||||||
return resolved;
|
return resolved;
|
||||||
}
|
}
|
||||||
@@ -200,46 +199,39 @@ public class ColoadGroupService implements ColoadGroupAssignmentPort {
|
|||||||
return row.getTenantId() != null && row.getTenantId() > 0
|
return row.getTenantId() != null && row.getTenantId() > 0
|
||||||
&& row.getInboundId() != null && row.getInboundId() > 0
|
&& row.getInboundId() != null && row.getInboundId() > 0
|
||||||
&& "WAIT_SPLIT".equals(row.getProcessStatus())
|
&& "WAIT_SPLIT".equals(row.getProcessStatus())
|
||||||
&& "MASTER".equals(row.getInboundRole())
|
|
||||||
&& "NOT_SPLIT".equals(row.getSplitStatus())
|
|
||||||
&& "MASTER".equals(row.getSplitPurpose())
|
|
||||||
&& row.getParentInboundId() == null
|
|
||||||
&& row.getFullPackageCount() != null && row.getFullPackageCount() > 0
|
&& row.getFullPackageCount() != null && row.getFullPackageCount() > 0
|
||||||
&& Objects.equals(row.getActiveAllocationCount(), 0)
|
&& Objects.equals(row.getActiveAllocationCount(), 0)
|
||||||
&& Objects.equals(row.getChildCount(), 0)
|
|
||||||
&& Objects.equals(row.getSplitBatchCount(), 0)
|
|
||||||
&& (row.getActiveGroupId() == null || Objects.equals(row.getActiveGroupId(), allowedGroupId));
|
&& (row.getActiveGroupId() == null || Objects.equals(row.getActiveGroupId(), allowedGroupId));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void validateSameRoute(List<InboundCandidateRow> rows) {
|
private void validateSameRoute(List<InboundCandidateRow> rows) {
|
||||||
InboundCandidateRow first = rows.get(0);
|
InboundCandidateRow first = rows.get(0);
|
||||||
if (first.getWarehouseId() == null || first.getWarehouseId() <= 0
|
if (first.getWarehouseId() == null || first.getWarehouseId() <= 0
|
||||||
|| !StringUtils.hasText(first.getChannelCode())
|
|| !StringUtils.hasText(first.getChannelCode())) {
|
||||||
|| !StringUtils.hasText(first.getDestinationType())
|
throw conflict("订单缺少国内仓或渠道权威快照");
|
||||||
|| destinationKey(first) == null) {
|
|
||||||
throw conflict("订单缺少仓库、渠道或目的仓港权威快照");
|
|
||||||
}
|
}
|
||||||
for (InboundCandidateRow row : rows) {
|
for (InboundCandidateRow row : rows) {
|
||||||
if (!Objects.equals(first.getWarehouseId(), row.getWarehouseId())
|
if (!Objects.equals(first.getWarehouseId(), row.getWarehouseId())
|
||||||
|| !normalized(first.getChannelCode()).equals(normalized(row.getChannelCode()))
|
|| !normalized(first.getChannelCode()).equals(normalized(row.getChannelCode()))) {
|
||||||
|| !normalized(first.getDestinationType()).equals(normalized(row.getDestinationType()))
|
throw conflict("同柜不可拆组成员必须属于同一国内仓和渠道");
|
||||||
|| !Objects.equals(destinationKey(first), destinationKey(row))) {
|
|
||||||
throw conflict("同柜不可拆组成员必须属于同一国内仓、渠道和目的仓港");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void validateStillAssignable(GroupSnapshot group) {
|
private void validateStillAssignable(GroupSnapshot group) {
|
||||||
List<Long> orderIds = group.members().stream().map(Member::orderId).toList();
|
List<Long> orderIds = group.members().stream().map(Member::orderId).distinct().sorted().toList();
|
||||||
List<InboundCandidateRow> current = authoritativeCandidates(orderIds, group.groupId());
|
List<InboundCandidateRow> current = authoritativeCandidates(orderIds, group.groupId());
|
||||||
Map<Long, Member> frozen = new LinkedHashMap<>();
|
Map<Long, Member> frozen = new LinkedHashMap<>();
|
||||||
group.members().forEach(member -> frozen.put(member.orderId(), member));
|
group.members().forEach(member -> frozen.put(member.inboundId(), member));
|
||||||
|
if (current.size() != frozen.size()) {
|
||||||
|
throw conflict("同柜不可拆组的可用入库片段集合已变化");
|
||||||
|
}
|
||||||
for (InboundCandidateRow row : current) {
|
for (InboundCandidateRow row : current) {
|
||||||
Member member = frozen.get(row.getOrderId());
|
Member member = frozen.get(row.getInboundId());
|
||||||
if (member == null || member.tenantId() != row.getTenantId()
|
if (member == null || member.tenantId() != row.getTenantId()
|
||||||
|| member.inboundId() != row.getInboundId()
|
|| member.orderId() != row.getOrderId()
|
||||||
|| member.fullPackageCount() != row.getFullPackageCount()) {
|
|| member.fullPackageCount() != row.getFullPackageCount()) {
|
||||||
throw conflict("同柜不可拆组成员的当前入库单或满箱数已变化");
|
throw conflict("同柜不可拆组的入库片段或满箱数已变化");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
validateSameRoute(current);
|
validateSameRoute(current);
|
||||||
@@ -257,8 +249,7 @@ public class ColoadGroupService implements ColoadGroupAssignmentPort {
|
|||||||
value.getFullPackageCount()))
|
value.getFullPackageCount()))
|
||||||
.toList();
|
.toList();
|
||||||
return new GroupSnapshot(row.getId(), row.getGroupNo(), row.getGroupStatus(), row.getWarehouseId(),
|
return new GroupSnapshot(row.getId(), row.getGroupNo(), row.getGroupStatus(), row.getWarehouseId(),
|
||||||
row.getChannelCode(), row.getDestinationType(), row.getDestinationWarehouseId(),
|
row.getChannelCode(), row.getLockedPlanId(), row.getVersion(), members,
|
||||||
row.getDestinationCode(), row.getLockedPlanId(), row.getVersion(), members,
|
|
||||||
members.stream().mapToInt(Member::fullPackageCount).sum(),
|
members.stream().mapToInt(Member::fullPackageCount).sum(),
|
||||||
row.getCreatedAt(), row.getUpdatedAt());
|
row.getCreatedAt(), row.getUpdatedAt());
|
||||||
}
|
}
|
||||||
@@ -303,14 +294,6 @@ public class ColoadGroupService implements ColoadGroupAssignmentPort {
|
|||||||
operationId, trim(remark), operatorId, trim(operatorName));
|
operationId, trim(remark), operatorId, trim(operatorName));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String destinationKey(InboundCandidateRow row) {
|
|
||||||
if (row.getDestinationWarehouseId() != null && row.getDestinationWarehouseId() > 0) {
|
|
||||||
return "ID:" + row.getDestinationWarehouseId();
|
|
||||||
}
|
|
||||||
String code = trim(row.getDestinationCode());
|
|
||||||
return code == null ? null : "CODE:" + code.toUpperCase(Locale.ROOT);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static List<Long> normalizedOrderIds(List<Long> source, int minimum, int maximum) {
|
private static List<Long> normalizedOrderIds(List<Long> source, int minimum, int maximum) {
|
||||||
if (source == null) throw badRequest("orderIds不能为空");
|
if (source == null) throw badRequest("orderIds不能为空");
|
||||||
List<Long> values = source.stream().filter(Objects::nonNull).distinct().sorted().toList();
|
List<Long> values = source.stream().filter(Objects::nonNull).distinct().sorted().toList();
|
||||||
|
|||||||
@@ -6,9 +6,6 @@ CREATE TABLE IF NOT EXISTS fl_coload_group (
|
|||||||
group_status VARCHAR(16) NOT NULL COMMENT '组状态:ACTIVE、LOCKED、CONSUMED、CANCELLED',
|
group_status VARCHAR(16) NOT NULL COMMENT '组状态:ACTIVE、LOCKED、CONSUMED、CANCELLED',
|
||||||
warehouse_id BIGINT UNSIGNED NOT NULL COMMENT '国内收货仓ID快照',
|
warehouse_id BIGINT UNSIGNED NOT NULL COMMENT '国内收货仓ID快照',
|
||||||
channel_code VARCHAR(64) NOT NULL COMMENT '物流渠道编码快照',
|
channel_code VARCHAR(64) NOT NULL COMMENT '物流渠道编码快照',
|
||||||
destination_type VARCHAR(32) NOT NULL COMMENT '目的类型快照',
|
|
||||||
destination_warehouse_id BIGINT UNSIGNED NULL COMMENT '目的仓ID快照',
|
|
||||||
destination_code VARCHAR(64) NULL COMMENT '目的仓或目的港编码快照',
|
|
||||||
locked_plan_id BIGINT UNSIGNED NULL COMMENT 'LOCKED/CONSUMED绑定装柜计划ID',
|
locked_plan_id BIGINT UNSIGNED NULL COMMENT 'LOCKED/CONSUMED绑定装柜计划ID',
|
||||||
version INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '乐观版本',
|
version INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '乐观版本',
|
||||||
created_by BIGINT UNSIGNED NOT NULL COMMENT '创建人ID',
|
created_by BIGINT UNSIGNED NOT NULL COMMENT '创建人ID',
|
||||||
@@ -30,8 +27,6 @@ CREATE TABLE IF NOT EXISTS fl_coload_group (
|
|||||||
KEY idx_coload_group_plan (locked_plan_id,group_status),
|
KEY idx_coload_group_plan (locked_plan_id,group_status),
|
||||||
CONSTRAINT fk_coload_group_plan FOREIGN KEY (locked_plan_id) REFERENCES fl_loading_plan(id),
|
CONSTRAINT fk_coload_group_plan FOREIGN KEY (locked_plan_id) REFERENCES fl_loading_plan(id),
|
||||||
CONSTRAINT chk_coload_group_status CHECK (group_status IN ('ACTIVE','LOCKED','CONSUMED','CANCELLED')),
|
CONSTRAINT chk_coload_group_status CHECK (group_status IN ('ACTIVE','LOCKED','CONSUMED','CANCELLED')),
|
||||||
CONSTRAINT chk_coload_group_destination CHECK (
|
|
||||||
destination_warehouse_id IS NOT NULL OR destination_code IS NOT NULL),
|
|
||||||
CONSTRAINT chk_coload_group_lock CHECK (
|
CONSTRAINT chk_coload_group_lock CHECK (
|
||||||
(group_status IN ('ACTIVE','CANCELLED') AND locked_plan_id IS NULL)
|
(group_status IN ('ACTIVE','CANCELLED') AND locked_plan_id IS NULL)
|
||||||
OR (group_status IN ('LOCKED','CONSUMED') AND locked_plan_id IS NOT NULL))
|
OR (group_status IN ('LOCKED','CONSUMED') AND locked_plan_id IS NOT NULL))
|
||||||
|
|||||||
@@ -2,14 +2,14 @@ openapi: 3.0.3
|
|||||||
info:
|
info:
|
||||||
title: Firstleg Coload Group API
|
title: Firstleg Coload Group API
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
description: 同柜且不可拆组平台管理契约;跨客户成组,但仓库、渠道和目的仓港必须一致。
|
description: 同柜且不可拆组平台管理契约;允许跨客户及不同目的仓港成组,仅要求国内仓和渠道一致。
|
||||||
paths:
|
paths:
|
||||||
/api/platform/v1/first-leg/coload-groups:
|
/api/platform/v1/first-leg/coload-groups:
|
||||||
post:
|
post:
|
||||||
summary: 创建同柜不可拆组
|
summary: 创建同柜不可拆组
|
||||||
description: >-
|
description: >-
|
||||||
需要 platform:firstleg:smart-loading:assign。orderIds 至少2项;服务端解析每个订单唯一当前权威入库单,
|
需要 platform:firstleg:smart-loading:assign。orderIds 至少2项;服务端解析每个订单全部 WAIT_SPLIT、
|
||||||
要求 WAIT_SPLIT、原始MASTER、未拆分、未分配、无子单或拆分批次,并冻结全部成员满箱数。
|
未分配且未属于其他活动组的可用入库片段,要求国内仓和渠道一致,并冻结全部片段及其满箱数。
|
||||||
parameters:
|
parameters:
|
||||||
- $ref: '#/components/parameters/IdempotencyKey'
|
- $ref: '#/components/parameters/IdempotencyKey'
|
||||||
requestBody:
|
requestBody:
|
||||||
@@ -130,23 +130,19 @@ components:
|
|||||||
ColoadGroup:
|
ColoadGroup:
|
||||||
type: object
|
type: object
|
||||||
additionalProperties: false
|
additionalProperties: false
|
||||||
required: [groupId, groupNo, status, warehouseId, channelCode, destinationType,
|
required: [groupId, groupNo, status, warehouseId, channelCode, version, members,
|
||||||
version, members, fullPackageCount, createdAt, updatedAt]
|
fullPackageCount, createdAt, updatedAt]
|
||||||
properties:
|
properties:
|
||||||
groupId: {type: string, pattern: '^[1-9][0-9]*$'}
|
groupId: {type: string, pattern: '^[1-9][0-9]*$'}
|
||||||
groupNo: {type: string, pattern: '^CLG-[1-9][0-9]*$'}
|
groupNo: {type: string, pattern: '^CLG-[1-9][0-9]*$'}
|
||||||
status: {type: string, enum: [ACTIVE, LOCKED, CONSUMED, CANCELLED]}
|
status: {type: string, enum: [ACTIVE, LOCKED, CONSUMED, CANCELLED]}
|
||||||
warehouseId: {type: string, pattern: '^[1-9][0-9]*$'}
|
warehouseId: {type: string, pattern: '^[1-9][0-9]*$'}
|
||||||
channelCode: {type: string}
|
channelCode: {type: string}
|
||||||
destinationType: {type: string}
|
|
||||||
destinationWarehouseId: {type: string, nullable: true}
|
|
||||||
destinationCode: {type: string, nullable: true}
|
|
||||||
lockedPlanId: {type: string, nullable: true}
|
lockedPlanId: {type: string, nullable: true}
|
||||||
version: {type: integer, minimum: 0}
|
version: {type: integer, minimum: 0}
|
||||||
members:
|
members:
|
||||||
type: array
|
type: array
|
||||||
minItems: 2
|
minItems: 2
|
||||||
maxItems: 100
|
|
||||||
items: {$ref: '#/components/schemas/ColoadGroupMember'}
|
items: {$ref: '#/components/schemas/ColoadGroupMember'}
|
||||||
fullPackageCount: {type: integer, minimum: 2, description: 全部成员满箱数之和}
|
fullPackageCount: {type: integer, minimum: 2, description: 全部成员满箱数之和}
|
||||||
createdAt: {type: string, format: date-time}
|
createdAt: {type: string, format: date-time}
|
||||||
@@ -154,8 +150,7 @@ components:
|
|||||||
OrderColoadGroupInfo:
|
OrderColoadGroupInfo:
|
||||||
type: object
|
type: object
|
||||||
additionalProperties: false
|
additionalProperties: false
|
||||||
required: [orderId, inboundId, group]
|
required: [orderId, group]
|
||||||
properties:
|
properties:
|
||||||
orderId: {type: string, pattern: '^[1-9][0-9]*$'}
|
orderId: {type: string, pattern: '^[1-9][0-9]*$'}
|
||||||
inboundId: {type: string, pattern: '^[1-9][0-9]*$'}
|
|
||||||
group: {$ref: '#/components/schemas/ColoadGroup'}
|
group: {$ref: '#/components/schemas/ColoadGroup'}
|
||||||
|
|||||||
+10
@@ -16,6 +16,10 @@ class ColoadGroupOpenApiContractTest {
|
|||||||
String source = Files.readString(Path.of("src/main/resources/openapi/coload-groups-v1.yaml"));
|
String source = Files.readString(Path.of("src/main/resources/openapi/coload-groups-v1.yaml"));
|
||||||
Map<String, Object> yaml = new Yaml().load(source);
|
Map<String, Object> yaml = new Yaml().load(source);
|
||||||
Map<String, Object> paths = (Map<String, Object>) yaml.get("paths");
|
Map<String, Object> paths = (Map<String, Object>) yaml.get("paths");
|
||||||
|
Map<String, Object> components = (Map<String, Object>) yaml.get("components");
|
||||||
|
Map<String, Object> schemas = (Map<String, Object>) components.get("schemas");
|
||||||
|
Map<String, Object> orderInfo = (Map<String, Object>) schemas.get("OrderColoadGroupInfo");
|
||||||
|
Map<String, Object> orderInfoProperties = (Map<String, Object>) orderInfo.get("properties");
|
||||||
|
|
||||||
assertThat(paths).containsKeys(
|
assertThat(paths).containsKeys(
|
||||||
"/api/platform/v1/first-leg/coload-groups",
|
"/api/platform/v1/first-leg/coload-groups",
|
||||||
@@ -32,6 +36,12 @@ class ColoadGroupOpenApiContractTest {
|
|||||||
.contains("expectedVersion")
|
.contains("expectedVersion")
|
||||||
.contains("fullPackageCount")
|
.contains("fullPackageCount")
|
||||||
.contains("platform:firstleg:smart-loading:assign")
|
.contains("platform:firstleg:smart-loading:assign")
|
||||||
|
.contains("全部 WAIT_SPLIT")
|
||||||
|
.contains("国内仓和渠道一致")
|
||||||
|
.doesNotContain("destinationType")
|
||||||
|
.doesNotContain("destinationWarehouseId")
|
||||||
|
.doesNotContain("destinationCode")
|
||||||
.doesNotContain("platform:firstleg:coload");
|
.doesNotContain("platform:firstleg:coload");
|
||||||
|
assertThat(orderInfoProperties).containsOnlyKeys("orderId", "group");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+28
-30
@@ -34,11 +34,12 @@ class ColoadGroupServiceTest {
|
|||||||
private ColoadGroupService service;
|
private ColoadGroupService service;
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void createsCrossCustomerGroupAndFreezesEveryMembersFullPackageCount() {
|
void createsCrossCustomerGroupAndFreezesAllAvailableFragmentsForEveryOrder() {
|
||||||
when(mapper.lockCandidates(List.of(11L, 12L))).thenReturn(List.of(
|
when(mapper.lockCandidates(List.of(11L, 12L))).thenReturn(List.of(
|
||||||
candidate(101L, 11L, 1001L, 30), candidate(202L, 12L, 1002L, 50)));
|
candidate(101L, 11L, 1001L, 30), candidate(101L, 11L, 1003L, 20),
|
||||||
when(mapper.insertGroup(anyLong(), anyString(), eq(9L), eq("CH"), eq("FBA"),
|
candidate(202L, 12L, 1002L, 50)));
|
||||||
eq(88L), eq("ONT8"), eq(7L), eq("operator"))).thenReturn(1);
|
when(mapper.insertGroup(anyLong(), anyString(), eq(9L), eq("CH"),
|
||||||
|
eq(7L), eq("operator"))).thenReturn(1);
|
||||||
when(mapper.insertMember(anyLong(), anyLong(), anyLong(), anyLong(), anyLong(), anyInt()))
|
when(mapper.insertMember(anyLong(), anyLong(), anyLong(), anyLong(), anyLong(), anyInt()))
|
||||||
.thenReturn(1);
|
.thenReturn(1);
|
||||||
when(mapper.insertHistory(anyLong(), anyLong(), anyString(), any(), anyString(), any(),
|
when(mapper.insertHistory(anyLong(), anyLong(), anyString(), any(), anyString(), any(),
|
||||||
@@ -48,17 +49,20 @@ class ColoadGroupServiceTest {
|
|||||||
when(mapper.completeCommand(anyLong(), anyLong())).thenReturn(1);
|
when(mapper.completeCommand(anyLong(), anyLong())).thenReturn(1);
|
||||||
when(mapper.group(anyLong())).thenReturn(group(500L, "ACTIVE", null, 0));
|
when(mapper.group(anyLong())).thenReturn(group(500L, "ACTIVE", null, 0));
|
||||||
when(mapper.members(anyLong())).thenReturn(List.of(
|
when(mapper.members(anyLong())).thenReturn(List.of(
|
||||||
member(101L, 11L, 1001L, 30), member(202L, 12L, 1002L, 50)));
|
member(101L, 11L, 1001L, 30), member(101L, 11L, 1003L, 20),
|
||||||
|
member(202L, 12L, 1002L, 50)));
|
||||||
|
|
||||||
var result = service.create(List.of(12L, 11L), "create-key", 7L, "operator");
|
var result = service.create(List.of(12L, 11L), "create-key", 7L, "operator");
|
||||||
|
|
||||||
assertThat(result.members()).extracting("tenantId", "orderId", "inboundId", "fullPackageCount")
|
assertThat(result.members()).extracting("tenantId", "orderId", "inboundId", "fullPackageCount")
|
||||||
.containsExactly(tuple(101L, 11L, 1001L, 30), tuple(202L, 12L, 1002L, 50));
|
.containsExactly(tuple(101L, 11L, 1001L, 30), tuple(101L, 11L, 1003L, 20),
|
||||||
assertThat(result.fullPackageCount()).isEqualTo(80);
|
tuple(202L, 12L, 1002L, 50));
|
||||||
|
assertThat(result.fullPackageCount()).isEqualTo(100);
|
||||||
ArgumentCaptor<Long> groupId = ArgumentCaptor.forClass(Long.class);
|
ArgumentCaptor<Long> groupId = ArgumentCaptor.forClass(Long.class);
|
||||||
verify(mapper).insertGroup(groupId.capture(), anyString(), eq(9L), eq("CH"), eq("FBA"),
|
verify(mapper).insertGroup(groupId.capture(), anyString(), eq(9L), eq("CH"),
|
||||||
eq(88L), eq("ONT8"), eq(7L), eq("operator"));
|
eq(7L), eq("operator"));
|
||||||
verify(mapper).insertMember(anyLong(), eq(groupId.getValue()), eq(101L), eq(11L), eq(1001L), eq(30));
|
verify(mapper).insertMember(anyLong(), eq(groupId.getValue()), eq(101L), eq(11L), eq(1001L), eq(30));
|
||||||
|
verify(mapper).insertMember(anyLong(), eq(groupId.getValue()), eq(101L), eq(11L), eq(1003L), eq(20));
|
||||||
verify(mapper).insertMember(anyLong(), eq(groupId.getValue()), eq(202L), eq(12L), eq(1002L), eq(50));
|
verify(mapper).insertMember(anyLong(), eq(groupId.getValue()), eq(202L), eq(12L), eq(1002L), eq(50));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,11 +75,9 @@ class ColoadGroupServiceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void rejectsMemberThatIsAllocatedSplitOrAlreadyGrouped() {
|
void rejectsOrderWhenNoAvailableFragmentRemains() {
|
||||||
InboundCandidateRow invalid = candidate(101L, 11L, 1001L, 30);
|
InboundCandidateRow invalid = candidate(101L, 11L, 1001L, 30);
|
||||||
invalid.setActiveAllocationCount(1);
|
invalid.setActiveAllocationCount(1);
|
||||||
invalid.setChildCount(1);
|
|
||||||
invalid.setSplitBatchCount(1);
|
|
||||||
invalid.setActiveGroupId(900L);
|
invalid.setActiveGroupId(900L);
|
||||||
when(mapper.insertCommand(anyLong(), eq(7L), eq("CREATE"), eq("key"), anyString())).thenReturn(1);
|
when(mapper.insertCommand(anyLong(), eq(7L), eq("CREATE"), eq("key"), anyString())).thenReturn(1);
|
||||||
when(mapper.lockCandidates(List.of(11L, 12L))).thenReturn(List.of(
|
when(mapper.lockCandidates(List.of(11L, 12L))).thenReturn(List.of(
|
||||||
@@ -83,16 +85,15 @@ class ColoadGroupServiceTest {
|
|||||||
|
|
||||||
assertThatThrownBy(() -> service.create(List.of(11L, 12L), "key", 7L, "operator"))
|
assertThatThrownBy(() -> service.create(List.of(11L, 12L), "key", 7L, "operator"))
|
||||||
.isInstanceOf(BizException.class)
|
.isInstanceOf(BizException.class)
|
||||||
.hasMessageContaining("唯一可创建");
|
.hasMessageContaining("不存在可创建");
|
||||||
verify(mapper, never()).insertGroup(anyLong(), anyString(), anyLong(), anyString(), anyString(),
|
verify(mapper, never()).insertGroup(anyLong(), anyString(), anyLong(), anyString(),
|
||||||
any(), any(), anyLong(), any());
|
anyLong(), any());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void rejectsDifferentWarehouseChannelOrDestination() {
|
void rejectsDifferentWarehouseOrChannel() {
|
||||||
InboundCandidateRow second = candidate(202L, 12L, 1002L, 50);
|
InboundCandidateRow second = candidate(202L, 12L, 1002L, 50);
|
||||||
second.setDestinationWarehouseId(99L);
|
second.setWarehouseId(10L);
|
||||||
second.setDestinationCode("LAX9");
|
|
||||||
when(mapper.insertCommand(anyLong(), eq(7L), eq("CREATE"), eq("route-key"), anyString()))
|
when(mapper.insertCommand(anyLong(), eq(7L), eq("CREATE"), eq("route-key"), anyString()))
|
||||||
.thenReturn(1);
|
.thenReturn(1);
|
||||||
when(mapper.lockCandidates(List.of(11L, 12L))).thenReturn(List.of(
|
when(mapper.lockCandidates(List.of(11L, 12L))).thenReturn(List.of(
|
||||||
@@ -100,7 +101,7 @@ class ColoadGroupServiceTest {
|
|||||||
|
|
||||||
assertThatThrownBy(() -> service.create(List.of(11L, 12L), "route-key", 7L, "operator"))
|
assertThatThrownBy(() -> service.create(List.of(11L, 12L), "route-key", 7L, "operator"))
|
||||||
.isInstanceOf(BizException.class)
|
.isInstanceOf(BizException.class)
|
||||||
.hasMessageContaining("同一国内仓、渠道和目的仓港");
|
.hasMessageContaining("同一国内仓和渠道");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -143,10 +144,12 @@ class ColoadGroupServiceTest {
|
|||||||
.thenReturn(1);
|
.thenReturn(1);
|
||||||
when(mapper.lockGroup(500L)).thenReturn(group(500L, "ACTIVE", null, 0));
|
when(mapper.lockGroup(500L)).thenReturn(group(500L, "ACTIVE", null, 0));
|
||||||
when(mapper.members(500L)).thenReturn(List.of(
|
when(mapper.members(500L)).thenReturn(List.of(
|
||||||
member(101L, 11L, 1001L, 30), member(202L, 12L, 1002L, 50)));
|
member(101L, 11L, 1001L, 30), member(101L, 11L, 1003L, 20),
|
||||||
|
member(202L, 12L, 1002L, 50)));
|
||||||
InboundCandidateRow first = candidate(101L, 11L, 1001L, 30); first.setActiveGroupId(500L);
|
InboundCandidateRow first = candidate(101L, 11L, 1001L, 30); first.setActiveGroupId(500L);
|
||||||
|
InboundCandidateRow fragment = candidate(101L, 11L, 1003L, 20); fragment.setActiveGroupId(500L);
|
||||||
InboundCandidateRow second = candidate(202L, 12L, 1002L, 50); second.setActiveGroupId(500L);
|
InboundCandidateRow second = candidate(202L, 12L, 1002L, 50); second.setActiveGroupId(500L);
|
||||||
when(mapper.lockCandidates(List.of(11L, 12L))).thenReturn(List.of(first, second));
|
when(mapper.lockCandidates(List.of(11L, 12L))).thenReturn(List.of(first, fragment, second));
|
||||||
when(mapper.lockToPlan(500L, 700L, 7L, "operator")).thenReturn(1);
|
when(mapper.lockToPlan(500L, 700L, 7L, "operator")).thenReturn(1);
|
||||||
when(mapper.completeCommand(anyLong(), eq(500L))).thenReturn(1);
|
when(mapper.completeCommand(anyLong(), eq(500L))).thenReturn(1);
|
||||||
when(mapper.group(500L)).thenReturn(group(500L, "LOCKED", 700L, 1));
|
when(mapper.group(500L)).thenReturn(group(500L, "LOCKED", 700L, 1));
|
||||||
@@ -156,8 +159,8 @@ class ColoadGroupServiceTest {
|
|||||||
|
|
||||||
assertThat(result.status()).isEqualTo("LOCKED");
|
assertThat(result.status()).isEqualTo("LOCKED");
|
||||||
assertThat(result.lockedPlanId()).isEqualTo(700L);
|
assertThat(result.lockedPlanId()).isEqualTo(700L);
|
||||||
assertThat(result.members()).hasSize(2);
|
assertThat(result.members()).hasSize(3);
|
||||||
assertThat(result.fullPackageCount()).isEqualTo(80);
|
assertThat(result.fullPackageCount()).isEqualTo(100);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -184,20 +187,15 @@ class ColoadGroupServiceTest {
|
|||||||
private static InboundCandidateRow candidate(long tenantId, long orderId, long inboundId, int packages) {
|
private static InboundCandidateRow candidate(long tenantId, long orderId, long inboundId, int packages) {
|
||||||
InboundCandidateRow value = new InboundCandidateRow();
|
InboundCandidateRow value = new InboundCandidateRow();
|
||||||
value.setTenantId(tenantId); value.setOrderId(orderId); value.setInboundId(inboundId);
|
value.setTenantId(tenantId); value.setOrderId(orderId); value.setInboundId(inboundId);
|
||||||
value.setProcessStatus("WAIT_SPLIT"); value.setInboundRole("MASTER");
|
value.setProcessStatus("WAIT_SPLIT"); value.setWarehouseId(9L); value.setChannelCode("CH");
|
||||||
value.setSplitStatus("NOT_SPLIT"); value.setSplitPurpose("MASTER");
|
|
||||||
value.setWarehouseId(9L); value.setChannelCode("CH"); value.setDestinationType("FBA");
|
|
||||||
value.setDestinationWarehouseId(88L); value.setDestinationCode("ONT8");
|
|
||||||
value.setFullPackageCount(packages); value.setActiveAllocationCount(0);
|
value.setFullPackageCount(packages); value.setActiveAllocationCount(0);
|
||||||
value.setChildCount(0); value.setSplitBatchCount(0);
|
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static GroupRow group(long id, String status, Long planId, int version) {
|
private static GroupRow group(long id, String status, Long planId, int version) {
|
||||||
GroupRow value = new GroupRow();
|
GroupRow value = new GroupRow();
|
||||||
value.setId(id); value.setGroupNo("CLG-" + id); value.setGroupStatus(status);
|
value.setId(id); value.setGroupNo("CLG-" + id); value.setGroupStatus(status);
|
||||||
value.setWarehouseId(9L); value.setChannelCode("CH"); value.setDestinationType("FBA");
|
value.setWarehouseId(9L); value.setChannelCode("CH");
|
||||||
value.setDestinationWarehouseId(88L); value.setDestinationCode("ONT8");
|
|
||||||
value.setLockedPlanId(planId); value.setVersion(version);
|
value.setLockedPlanId(planId); value.setVersion(version);
|
||||||
value.setCreatedAt(LocalDateTime.of(2026, 8, 29, 9, 0));
|
value.setCreatedAt(LocalDateTime.of(2026, 8, 29, 9, 0));
|
||||||
value.setUpdatedAt(value.getCreatedAt());
|
value.setUpdatedAt(value.getCreatedAt());
|
||||||
|
|||||||
+21
-5
@@ -27,11 +27,16 @@ class ColoadGroupSqlContractTest {
|
|||||||
.contains("full_package_count")
|
.contains("full_package_count")
|
||||||
.contains("uk_coload_command")
|
.contains("uk_coload_command")
|
||||||
.contains("locked_plan_id");
|
.contains("locked_plan_id");
|
||||||
|
assertThat(tableDefinition(source, "fl_coload_group"))
|
||||||
|
.doesNotContain("chk_coload_group_destination")
|
||||||
|
.doesNotContain("destination_type")
|
||||||
|
.doesNotContain("destination_warehouse_id")
|
||||||
|
.doesNotContain("destination_code");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void mapperLocksAuthoritativeInboundAndRejectsAllocationChildrenAndSplitFacts() throws Exception {
|
void mapperLocksEveryInboundFragmentAndOnlyProjectsCurrentAllocationAndGroupFacts() throws Exception {
|
||||||
assertThat(ColoadGroupMapper.class.getAnnotation(Mapper.class)).isNotNull();
|
assertThat(ColoadGroupMapper.class.getAnnotation(Mapper.class)).isNotNull();
|
||||||
Method method = ColoadGroupMapper.class.getDeclaredMethod("lockCandidates", java.util.List.class);
|
Method method = ColoadGroupMapper.class.getDeclaredMethod("lockCandidates", java.util.List.class);
|
||||||
String sql = String.join(" ", method.getAnnotation(Select.class).value());
|
String sql = String.join(" ", method.getAnnotation(Select.class).value());
|
||||||
@@ -39,10 +44,21 @@ class ColoadGroupSqlContractTest {
|
|||||||
.contains("FROM fl_inbound_order o")
|
.contains("FROM fl_inbound_order o")
|
||||||
.contains("FROM fl_loading_plan_item item")
|
.contains("FROM fl_loading_plan_item item")
|
||||||
.contains("item.deleted=0")
|
.contains("item.deleted=0")
|
||||||
.contains("child.parent_inbound_id=o.id")
|
|
||||||
.contains("FROM fl_inbound_split_batch batch")
|
|
||||||
.contains("batch.status IN ('PREPARED','APPLIED')")
|
|
||||||
.contains("member.active_flag=1")
|
.contains("member.active_flag=1")
|
||||||
.contains("FOR UPDATE");
|
.contains("FOR UPDATE")
|
||||||
|
.doesNotContain("inbound_role")
|
||||||
|
.doesNotContain("split_status")
|
||||||
|
.doesNotContain("split_purpose")
|
||||||
|
.doesNotContain("parent_inbound_id")
|
||||||
|
.doesNotContain("fl_inbound_split_batch")
|
||||||
|
.doesNotContain("destination_type")
|
||||||
|
.doesNotContain("destination_warehouse_id")
|
||||||
|
.doesNotContain("destination_code");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String tableDefinition(String source, String tableName) {
|
||||||
|
int start = source.indexOf("CREATE TABLE IF NOT EXISTS " + tableName + " (");
|
||||||
|
int end = source.indexOf(';', start);
|
||||||
|
return source.substring(start, end + 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user