Merge branch 'test' of http://8.163.30.128:10082/java-project/baiyue-firstleg into test
This commit is contained in:
@@ -1,61 +0,0 @@
|
|||||||
-- 可靠审计 Outbox、消费幂等和死信补偿增量(MySQL 8)
|
|
||||||
SET NAMES utf8mb4;
|
|
||||||
USE baiyue_audit;
|
|
||||||
|
|
||||||
DROP PROCEDURE IF EXISTS create_reliable_audit_outbox;
|
|
||||||
DELIMITER $$
|
|
||||||
CREATE PROCEDURE create_reliable_audit_outbox(IN schema_name VARCHAR(64))
|
|
||||||
BEGIN
|
|
||||||
SET @ddl = CONCAT(
|
|
||||||
'CREATE TABLE IF NOT EXISTS \`', schema_name, '\`.sys_audit_outbox (',
|
|
||||||
'event_id VARCHAR(64) NOT NULL,event_type VARCHAR(64) NOT NULL,resource_type VARCHAR(64) NOT NULL,',
|
|
||||||
'resource_id VARCHAR(128) NULL,service_name VARCHAR(64) NULL,payload_json JSON NOT NULL,',
|
|
||||||
'status VARCHAR(16) NOT NULL DEFAULT ''PENDING'',attempt_count INT NOT NULL DEFAULT 0,',
|
|
||||||
'compensation_count INT NOT NULL DEFAULT 0,next_retry_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),',
|
|
||||||
'lease_owner VARCHAR(128) NULL,lease_until DATETIME(3) NULL,last_error VARCHAR(1000) NULL,',
|
|
||||||
'dead_at DATETIME(3) NULL,sent_at DATETIME(3) NULL,created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),',
|
|
||||||
'updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),',
|
|
||||||
'PRIMARY KEY(event_id),KEY idx_audit_outbox_publish(status,next_retry_at,lease_until,created_at),',
|
|
||||||
'KEY idx_audit_outbox_retention(status,sent_at)) ENGINE=InnoDB COMMENT=''可靠审计事件本地发送箱'''
|
|
||||||
);
|
|
||||||
PREPARE stmt FROM @ddl;
|
|
||||||
EXECUTE stmt;
|
|
||||||
DEALLOCATE PREPARE stmt;
|
|
||||||
END$$
|
|
||||||
DELIMITER ;
|
|
||||||
CALL create_reliable_audit_outbox('baiyue_auth');
|
|
||||||
CALL create_reliable_audit_outbox('baiyue_iam');
|
|
||||||
CALL create_reliable_audit_outbox('baiyue_order');
|
|
||||||
CALL create_reliable_audit_outbox('baiyue_firstleg');
|
|
||||||
CALL create_reliable_audit_outbox('baiyue_warehouse');
|
|
||||||
DROP PROCEDURE IF EXISTS create_reliable_audit_outbox;
|
|
||||||
|
|
||||||
USE baiyue_audit;
|
|
||||||
DROP PROCEDURE IF EXISTS add_audit_event_id;
|
|
||||||
DELIMITER $$
|
|
||||||
CREATE PROCEDURE add_audit_event_id()
|
|
||||||
BEGIN
|
|
||||||
IF NOT EXISTS (SELECT 1 FROM information_schema.COLUMNS
|
|
||||||
WHERE TABLE_SCHEMA='baiyue_audit' AND TABLE_NAME='sys_audit_log' AND COLUMN_NAME='event_id') THEN
|
|
||||||
ALTER TABLE sys_audit_log ADD COLUMN event_id VARCHAR(64) NULL COMMENT '审计事件全局唯一ID' AFTER id;
|
|
||||||
END IF;
|
|
||||||
IF NOT EXISTS (SELECT 1 FROM information_schema.STATISTICS
|
|
||||||
WHERE TABLE_SCHEMA='baiyue_audit' AND TABLE_NAME='sys_audit_log' AND INDEX_NAME='uk_audit_event_id') THEN
|
|
||||||
ALTER TABLE sys_audit_log ADD UNIQUE KEY uk_audit_event_id(event_id);
|
|
||||||
END IF;
|
|
||||||
END$$
|
|
||||||
DELIMITER ;
|
|
||||||
CALL add_audit_event_id();
|
|
||||||
DROP PROCEDURE IF EXISTS add_audit_event_id;
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS sys_audit_dead_letter (
|
|
||||||
id BIGINT NOT NULL AUTO_INCREMENT,event_id VARCHAR(64) NOT NULL,payload_text LONGTEXT NOT NULL,
|
|
||||||
status VARCHAR(16) NOT NULL DEFAULT 'PENDING',retry_count INT NOT NULL DEFAULT 0,
|
|
||||||
next_retry_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),lease_owner VARCHAR(128) NULL,
|
|
||||||
lease_until DATETIME(3) NULL,last_error VARCHAR(1000) NULL,
|
|
||||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),processed_at DATETIME(3) NULL,
|
|
||||||
PRIMARY KEY(id),UNIQUE KEY uk_audit_dead_event(event_id),
|
|
||||||
KEY idx_audit_dead_retry(status,next_retry_at,lease_until)
|
|
||||||
) ENGINE=InnoDB COMMENT='审计消息死信补偿表';
|
|
||||||
|
|
||||||
SELECT 'PASS' AS migration_status,'可靠审计Outbox、幂等和死信补偿表已就绪' AS message;
|
|
||||||
@@ -63,7 +63,7 @@ public interface ContainerBookingMapper extends BaseMapper<ContainerBookingEntit
|
|||||||
updated_by_name=#{operatorName}, updated_at=CURRENT_TIMESTAMP(3), version=version+1
|
updated_by_name=#{operatorName}, updated_at=CURRENT_TIMESTAMP(3), version=version+1
|
||||||
WHERE id=#{value.id} AND version=#{expectedVersion}
|
WHERE id=#{value.id} AND version=#{expectedVersion}
|
||||||
AND NOT EXISTS (SELECT 1 FROM fl_loading_plan p
|
AND NOT EXISTS (SELECT 1 FROM fl_loading_plan p
|
||||||
WHERE p.container_booking_id=fl_container_booking.id AND p.plan_status<>'EDITING')
|
WHERE p.container_booking_id=fl_container_booking.id AND p.plan_status!='EDITING')
|
||||||
""")
|
""")
|
||||||
int updateDraft(@Param("value") ContainerBookingEntity value, @Param("expectedVersion") int expectedVersion,
|
int updateDraft(@Param("value") ContainerBookingEntity value, @Param("expectedVersion") int expectedVersion,
|
||||||
@Param("operatorId") long operatorId, @Param("operatorName") String operatorName);
|
@Param("operatorId") long operatorId, @Param("operatorName") String operatorName);
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import java.util.*;
|
|||||||
/** 智能装柜的候选、推荐/人工分配、删除和不可逆推送编排。 */
|
/** 智能装柜的候选、推荐/人工分配、删除和不可逆推送编排。 */
|
||||||
@Service
|
@Service
|
||||||
public class SmartLoadingService {
|
public class SmartLoadingService {
|
||||||
|
private static final BigDecimal CAPACITY_LIMIT_RATIO = new BigDecimal("1.10");
|
||||||
@Resource private SmartLoadingRepository repository;
|
@Resource private SmartLoadingRepository repository;
|
||||||
@Resource private OrderCargoClient orderCargoClient;
|
@Resource private OrderCargoClient orderCargoClient;
|
||||||
@Resource private LoadingPickService loadingPickService;
|
@Resource private LoadingPickService loadingPickService;
|
||||||
@@ -218,9 +219,12 @@ public class SmartLoadingService {
|
|||||||
if ("LOADING".equals(plan.getPlanStatus()) && key.equals(plan.getPushIdempotencyKey())) return view(plan, requireBooking(plan.getContainerBookingId()));
|
if ("LOADING".equals(plan.getPlanStatus()) && key.equals(plan.getPushIdempotencyKey())) return view(plan, requireBooking(plan.getContainerBookingId()));
|
||||||
if (!"EDITING".equals(plan.getPlanStatus()) || !Objects.equals(plan.getVersion(), request.expectedVersion())) throw conflict("仅编辑中的计划可开始装柜且版本必须匹配");
|
if (!"EDITING".equals(plan.getPlanStatus()) || !Objects.equals(plan.getVersion(), request.expectedVersion())) throw conflict("仅编辑中的计划可开始装柜且版本必须匹配");
|
||||||
ContainerBookingEntity booking = requireBooking(plan.getContainerBookingId());
|
ContainerBookingEntity booking = requireBooking(plan.getContainerBookingId());
|
||||||
if (!StringUtils.hasText(booking.getContainerNo())) throw validation("开始装柜前必须填写集装箱号");
|
List<LoadingPlanItemEntity> items = repository.items(planId);
|
||||||
if (repository.items(planId).isEmpty()) throw validation("装柜明细不能为空");
|
if (items.isEmpty()) throw validation("装柜明细不能为空");
|
||||||
loadingPickService.begin(planId, booking.getContainerNo(), operatorId, operatorName, repository.items(planId));
|
checkPlanCapacity(booking, items);
|
||||||
|
if (StringUtils.hasText(booking.getContainerNo())) {
|
||||||
|
loadingPickService.begin(planId, booking.getContainerNo(), operatorId, operatorName, items);
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
if (repository.push(planId, key, request.expectedVersion(), operatorId, operatorName) != 1) throw conflict("装柜计划已被并发修改");
|
if (repository.push(planId, key, request.expectedVersion(), operatorId, operatorName) != 1) throw conflict("装柜计划已被并发修改");
|
||||||
} catch (DuplicateKeyException e) { throw conflict("幂等键已用于其他推送"); }
|
} catch (DuplicateKeyException e) { throw conflict("幂等键已用于其他推送"); }
|
||||||
@@ -287,13 +291,32 @@ public class SmartLoadingService {
|
|||||||
BigDecimal v=lines.stream().map(x->unit(x.getVolumeCbm())).reduce(BigDecimal.ZERO,BigDecimal::add).add(unit(c.unitVolumeCbm()).multiply(BigDecimal.valueOf(count)));
|
BigDecimal v=lines.stream().map(x->unit(x.getVolumeCbm())).reduce(BigDecimal.ZERO,BigDecimal::add).add(unit(c.unitVolumeCbm()).multiply(BigDecimal.valueOf(count)));
|
||||||
BigDecimal w=lines.stream().map(x->unit(x.getGrossWeightKg())).reduce(BigDecimal.ZERO,BigDecimal::add).add(unit(c.unitWeightKg()).multiply(BigDecimal.valueOf(count)));
|
BigDecimal w=lines.stream().map(x->unit(x.getGrossWeightKg())).reduce(BigDecimal.ZERO,BigDecimal::add).add(unit(c.unitWeightKg()).multiply(BigDecimal.valueOf(count)));
|
||||||
if (b.getCapacityCbm()==null || b.getMaxWeightKg()==null) throw validation("货柜容量和最大承重未配置");
|
if (b.getCapacityCbm()==null || b.getMaxWeightKg()==null) throw validation("货柜容量和最大承重未配置");
|
||||||
if (v.compareTo(b.getCapacityCbm())>0 || w.compareTo(b.getMaxWeightKg())>0) throw validation("超过货柜容积或重量上限");
|
if (v.compareTo(b.getCapacityCbm().multiply(CAPACITY_LIMIT_RATIO))>0
|
||||||
|
|| w.compareTo(b.getMaxWeightKg().multiply(CAPACITY_LIMIT_RATIO))>0) {
|
||||||
|
throw validation("超过货柜容积或重量的110%上限");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private void checkPlanCapacity(ContainerBookingEntity booking, List<LoadingPlanItemEntity> items) {
|
||||||
|
if (booking.getCapacityCbm() == null || booking.getMaxWeightKg() == null) {
|
||||||
|
throw validation("货柜容量和最大承重未配置");
|
||||||
|
}
|
||||||
|
BigDecimal weight = items.stream().map(x -> unit(x.getGrossWeightKg()))
|
||||||
|
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||||
|
if (weight.compareTo(booking.getMaxWeightKg().multiply(CAPACITY_LIMIT_RATIO)) > 0) {
|
||||||
|
throw validation("超过货柜重量的110%上限");
|
||||||
|
}
|
||||||
|
BigDecimal volume = items.stream().map(x -> unit(x.getVolumeCbm()))
|
||||||
|
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||||
|
if (volume.compareTo(booking.getCapacityCbm().multiply(CAPACITY_LIMIT_RATIO)) > 0) {
|
||||||
|
throw validation("超过货柜容积的110%上限");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
private int fittingBoxes(ContainerBookingEntity b,List<LoadingPlanItemEntity> lines,Candidate c){
|
private int fittingBoxes(ContainerBookingEntity b,List<LoadingPlanItemEntity> lines,Candidate c){
|
||||||
if(b.getCapacityCbm()==null||b.getMaxWeightKg()==null)return 0;
|
if(b.getCapacityCbm()==null||b.getMaxWeightKg()==null)return 0;
|
||||||
BigDecimal usedV=lines.stream().map(x->unit(x.getVolumeCbm())).reduce(BigDecimal.ZERO,BigDecimal::add);
|
BigDecimal usedV=lines.stream().map(x->unit(x.getVolumeCbm())).reduce(BigDecimal.ZERO,BigDecimal::add);
|
||||||
BigDecimal usedW=lines.stream().map(x->unit(x.getGrossWeightKg())).reduce(BigDecimal.ZERO,BigDecimal::add);
|
BigDecimal usedW=lines.stream().map(x->unit(x.getGrossWeightKg())).reduce(BigDecimal.ZERO,BigDecimal::add);
|
||||||
BigDecimal leftV=b.getCapacityCbm().subtract(usedV),leftW=b.getMaxWeightKg().subtract(usedW);
|
BigDecimal leftV=b.getCapacityCbm().multiply(CAPACITY_LIMIT_RATIO).subtract(usedV);
|
||||||
|
BigDecimal leftW=b.getMaxWeightKg().multiply(CAPACITY_LIMIT_RATIO).subtract(usedW);
|
||||||
int byV=unit(c.unitVolumeCbm()).signum()==0?c.availablePackageCount():leftV.max(BigDecimal.ZERO).divideToIntegralValue(c.unitVolumeCbm()).intValue();
|
int byV=unit(c.unitVolumeCbm()).signum()==0?c.availablePackageCount():leftV.max(BigDecimal.ZERO).divideToIntegralValue(c.unitVolumeCbm()).intValue();
|
||||||
int byW=unit(c.unitWeightKg()).signum()==0?c.availablePackageCount():leftW.max(BigDecimal.ZERO).divideToIntegralValue(c.unitWeightKg()).intValue();
|
int byW=unit(c.unitWeightKg()).signum()==0?c.availablePackageCount():leftW.max(BigDecimal.ZERO).divideToIntegralValue(c.unitWeightKg()).intValue();
|
||||||
return Math.max(0,Math.min(c.availablePackageCount(),Math.min(byV,byW)));
|
return Math.max(0,Math.min(c.availablePackageCount(),Math.min(byV,byW)));
|
||||||
@@ -301,7 +324,9 @@ public class SmartLoadingService {
|
|||||||
private Plan view(LoadingPlanEntity p,ContainerBookingEntity b){return new Plan(p.getId(),p.getPlanNo(),p.getPlanStatus(),p.getContainerBookingId(),b.getContainerNo(),b.getVoyageNo(),p.getVersion(),repository.items(p.getId()));}
|
private Plan view(LoadingPlanEntity p,ContainerBookingEntity b){return new Plan(p.getId(),p.getPlanNo(),p.getPlanStatus(),p.getContainerBookingId(),b.getContainerNo(),b.getVoyageNo(),p.getVersion(),repository.items(p.getId()));}
|
||||||
private Plan draftView(LoadingPlanEntity plan, ContainerBookingEntity booking) {
|
private Plan draftView(LoadingPlanEntity plan, ContainerBookingEntity booking) {
|
||||||
if (plan == null) throw new BizException(ResultCode.NOT_FOUND, "该货柜尚未创建智能装柜草稿");
|
if (plan == null) throw new BizException(ResultCode.NOT_FOUND, "该货柜尚未创建智能装柜草稿");
|
||||||
if (!"EDITING".equals(plan.getPlanStatus())) throw conflict("装柜计划已推送预装,请前往预装清单查看");
|
if (!Set.of("EDITING", "LOADING").contains(plan.getPlanStatus())) {
|
||||||
|
throw conflict("装柜已完成,请前往装柜完成记录查看");
|
||||||
|
}
|
||||||
return view(plan, booking);
|
return view(plan, booking);
|
||||||
}
|
}
|
||||||
private LoadingPlanEntity editing(long id,int version){LoadingPlanEntity p=repository.plan(id);if(p==null)throw new BizException(ResultCode.NOT_FOUND,"装柜计划不存在");if(!Set.of("EDITING", "LOADING").contains(p.getPlanStatus())||!Objects.equals(p.getVersion(),version))throw conflict("计划已完成或版本冲突");return p;}
|
private LoadingPlanEntity editing(long id,int version){LoadingPlanEntity p=repository.plan(id);if(p==null)throw new BizException(ResultCode.NOT_FOUND,"装柜计划不存在");if(!Set.of("EDITING", "LOADING").contains(p.getPlanStatus())||!Objects.equals(p.getVersion(),version))throw conflict("计划已完成或版本冲突");return p;}
|
||||||
|
|||||||
@@ -4,3 +4,8 @@ spring:
|
|||||||
url: ${FIRSTLEG_DB_URL:jdbc:mysql://127.0.0.1:3306/baiyue_firstleg?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true}
|
url: ${FIRSTLEG_DB_URL:jdbc:mysql://127.0.0.1:3306/baiyue_firstleg?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true}
|
||||||
username: ${FIRSTLEG_DB_USERNAME:root}
|
username: ${FIRSTLEG_DB_USERNAME:root}
|
||||||
password: ${FIRSTLEG_DB_PASSWORD:123456}
|
password: ${FIRSTLEG_DB_PASSWORD:123456}
|
||||||
|
|
||||||
|
# 仅本地开发环境输出 MyBatis 执行 SQL、参数与结果摘要到 IDEA 控制台。
|
||||||
|
mybatis-plus:
|
||||||
|
configuration:
|
||||||
|
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
|
||||||
|
|||||||
+4
-2
@@ -39,8 +39,10 @@ class ContainerBookingMapperContractTest {
|
|||||||
.reduce("", (left, right) -> left + "\n" + right);
|
.reduce("", (left, right) -> left + "\n" + right);
|
||||||
assertThat(allSql)
|
assertThat(allSql)
|
||||||
.contains("capacity_cbm")
|
.contains("capacity_cbm")
|
||||||
.contains("p.plan_status<>'EDITING'")
|
.contains("p.plan_status!='EDITING'")
|
||||||
.contains("p.plan_status='COMPLETED'")
|
.contains("p.plan_status='COMPLETED'")
|
||||||
.contains("version=#{expectedVersion}");
|
.contains("version=#{expectedVersion}")
|
||||||
|
.doesNotContain("<")
|
||||||
|
.doesNotContain(">");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
|||||||
import static org.assertj.core.api.Assertions.catchThrowableOfType;
|
import static org.assertj.core.api.Assertions.catchThrowableOfType;
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.ArgumentMatchers.anyInt;
|
import static org.mockito.ArgumentMatchers.anyInt;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyList;
|
||||||
import static org.mockito.ArgumentMatchers.anyLong;
|
import static org.mockito.ArgumentMatchers.anyLong;
|
||||||
import static org.mockito.ArgumentMatchers.anyString;
|
import static org.mockito.ArgumentMatchers.anyString;
|
||||||
import static org.mockito.ArgumentMatchers.eq;
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
@@ -77,6 +78,28 @@ class SmartLoadingServiceTest {
|
|||||||
assertThat(service.detail(1L).bookingId()).isEqualTo(99L);
|
assertThat(service.detail(1L).bookingId()).isEqualTo(99L);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void restoresLoadingPlanForContinuedAssignment() {
|
||||||
|
LoadingPlanEntity plan = plan("LOADING", 3);
|
||||||
|
when(repository.booking(99L)).thenReturn(booking());
|
||||||
|
when(repository.planByBooking(99L)).thenReturn(plan);
|
||||||
|
when(repository.items(1L)).thenReturn(List.of());
|
||||||
|
|
||||||
|
assertThat(service.draftByBooking(99L).status()).isEqualTo("LOADING");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void doesNotRestoreCompletedPlanInTheLoadingWorkbench() {
|
||||||
|
when(repository.booking(99L)).thenReturn(booking());
|
||||||
|
when(repository.planByBooking(99L)).thenReturn(plan("COMPLETED", 4));
|
||||||
|
|
||||||
|
BizException error = catchThrowableOfType(
|
||||||
|
() -> service.draftByBooking(99L), BizException.class);
|
||||||
|
|
||||||
|
assertThat(error.getResultCode()).isEqualTo(ResultCode.CONFLICT);
|
||||||
|
verify(repository, never()).items(anyLong());
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void returnsExistingDraftOnCreate() {
|
void returnsExistingDraftOnCreate() {
|
||||||
LoadingPlanEntity plan = plan("EDITING", 2);
|
LoadingPlanEntity plan = plan("EDITING", 2);
|
||||||
@@ -105,6 +128,35 @@ class SmartLoadingServiceTest {
|
|||||||
verify(repository, never()).touch(anyLong(), anyInt(), anyLong(), any());
|
verify(repository, never()).touch(anyLong(), anyInt(), anyLong(), any());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void allowsVolumeAndWeightWithinOneHundredTenPercent() {
|
||||||
|
stubAssignment();
|
||||||
|
ContainerBookingEntity tolerant = booking();
|
||||||
|
tolerant.setCapacityCbm(new BigDecimal("0.91"));
|
||||||
|
tolerant.setMaxWeightKg(new BigDecimal("3.64"));
|
||||||
|
when(repository.booking(99L)).thenReturn(tolerant);
|
||||||
|
|
||||||
|
service.assign(1, new AssignRequest(10L, 20L, 2, 0, "assign-110-ok"), 7, "u");
|
||||||
|
|
||||||
|
verify(repository).touch(1, 0, 7, "u");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void rejectsVolumeOrWeightAboveOneHundredTenPercent() {
|
||||||
|
stubAssignment();
|
||||||
|
ContainerBookingEntity exceeded = booking();
|
||||||
|
exceeded.setCapacityCbm(new BigDecimal("0.90"));
|
||||||
|
exceeded.setMaxWeightKg(new BigDecimal("3.63"));
|
||||||
|
when(repository.booking(99L)).thenReturn(exceeded);
|
||||||
|
|
||||||
|
BizException error = catchThrowableOfType(() -> service.assign(1,
|
||||||
|
new AssignRequest(10L, 20L, 2, 0, "assign-110-exceeded"), 7, "u"), BizException.class);
|
||||||
|
|
||||||
|
assertThat(error.getResultCode()).isEqualTo(ResultCode.VALIDATION_FAILED);
|
||||||
|
assertThat(error.getMessage()).contains("110%");
|
||||||
|
verify(repository, never()).touch(anyLong(), anyInt(), anyLong(), any());
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void snapshotsCargoFields() {
|
void snapshotsCargoFields() {
|
||||||
stubAssignment();
|
stubAssignment();
|
||||||
@@ -296,6 +348,46 @@ class SmartLoadingServiceTest {
|
|||||||
verify(repository).push(1, "key", 3, 7, "u");
|
verify(repository).push(1, "key", 3, 7, "u");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void rejectsOverweightPlanBeforePushEvenWithoutContainerNumber() {
|
||||||
|
LoadingPlanEntity editing = plan("EDITING", 1);
|
||||||
|
ContainerBookingEntity booking = booking();
|
||||||
|
booking.setContainerNo(null);
|
||||||
|
booking.setMaxWeightKg(new BigDecimal("1"));
|
||||||
|
LoadingPlanItemEntity overweight = item(8, 9, 20, 1);
|
||||||
|
overweight.setGrossWeightKg(new BigDecimal("2"));
|
||||||
|
when(repository.plan(1)).thenReturn(editing);
|
||||||
|
when(repository.booking(99)).thenReturn(booking);
|
||||||
|
when(repository.items(1)).thenReturn(List.of(overweight));
|
||||||
|
|
||||||
|
BizException error = catchThrowableOfType(
|
||||||
|
() -> service.pushPreload(1, "weight-key", new PushRequest(1), 7, "u"),
|
||||||
|
BizException.class);
|
||||||
|
|
||||||
|
assertThat(error.getResultCode()).isEqualTo(ResultCode.VALIDATION_FAILED);
|
||||||
|
assertThat(error.getMessage()).contains("重量").contains("110%");
|
||||||
|
verify(repository, never()).push(anyLong(), anyString(), anyInt(), anyLong(), anyString());
|
||||||
|
verify(loadingPickService, never()).begin(anyLong(), any(), anyLong(), anyString(), anyList());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void pushesWithoutContainerNumberWhenCapacityIsWithinLimit() {
|
||||||
|
LoadingPlanEntity editing = plan("EDITING", 1);
|
||||||
|
LoadingPlanEntity pushed = plan("LOADING", 2);
|
||||||
|
ContainerBookingEntity booking = booking();
|
||||||
|
booking.setContainerNo(null);
|
||||||
|
when(repository.plan(1)).thenReturn(editing, pushed);
|
||||||
|
when(repository.booking(99)).thenReturn(booking);
|
||||||
|
when(repository.items(1)).thenReturn(List.of(item(8, 9, 20, 1)));
|
||||||
|
when(repository.push(1, "no-container-key", 1, 7, "u")).thenReturn(1);
|
||||||
|
|
||||||
|
Plan result = service.pushPreload(1, "no-container-key", new PushRequest(1), 7, "u");
|
||||||
|
|
||||||
|
assertThat(result.status()).isEqualTo("LOADING");
|
||||||
|
verify(repository).push(1, "no-container-key", 1, 7, "u");
|
||||||
|
verify(loadingPickService, never()).begin(anyLong(), any(), anyLong(), anyString(), anyList());
|
||||||
|
}
|
||||||
|
|
||||||
private void stubAssignment() {
|
private void stubAssignment() {
|
||||||
LoadingPlanEntity plan = plan("EDITING", 0);
|
LoadingPlanEntity plan = plan("EDITING", 0);
|
||||||
when(repository.plan(1)).thenReturn(plan, plan("EDITING", 1));
|
when(repository.plan(1)).thenReturn(plan, plan("EDITING", 1));
|
||||||
|
|||||||
Reference in New Issue
Block a user