fix(firstleg): require real container before pda push
This commit is contained in:
@@ -8712,6 +8712,11 @@ CREATE TABLE IF NOT EXISTS fl_ocean_booking (
|
||||
OR pending_estimated_arrival_at >= pending_estimated_sailing_at)
|
||||
) ENGINE=InnoDB COMMENT='独立海运订舱聚合;柜次仍由fl_container_booking承载';
|
||||
|
||||
-- CREATE TABLE IF NOT EXISTS does not reconcile a database initialized before customer became optional.
|
||||
ALTER TABLE fl_ocean_booking
|
||||
MODIFY COLUMN customer_id BIGINT UNSIGNED NULL COMMENT '可选客户ID快照',
|
||||
MODIFY COLUMN customer_name VARCHAR(128) NULL COMMENT '可选客户名称快照';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fl_ocean_booking_party (
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '订舱提单关系方ID',
|
||||
ocean_booking_id BIGINT UNSIGNED NOT NULL COMMENT '所属海运订舱ID',
|
||||
|
||||
@@ -228,13 +228,16 @@ public class SmartLoadingService {
|
||||
String key = trim(idempotencyKey);
|
||||
if (!StringUtils.hasText(key) || key.length() > 128) throw new BizException(ResultCode.BAD_REQUEST, "必须提供有效 Idempotency-Key");
|
||||
LoadingPlanEntity plan = repository.plan(planId); if (plan == null) throw new BizException(ResultCode.NOT_FOUND, "装柜计划不存在");
|
||||
ContainerBookingEntity booking = requireBooking(plan.getContainerBookingId());
|
||||
if (!StringUtils.hasText(booking.getContainerNo())) {
|
||||
throw validation("录入真实柜号后才能推送预装清单并开启PDA作业");
|
||||
}
|
||||
if (key.equals(plan.getPushIdempotencyKey())) {
|
||||
loadingPickService.beginIfReady(plan.getContainerBookingId(), operatorId, operatorName);
|
||||
return view(plan, requireBooking(plan.getContainerBookingId()));
|
||||
return view(plan, booking);
|
||||
}
|
||||
if (StringUtils.hasText(plan.getPushIdempotencyKey())) throw conflict("预装清单已推送,不能使用新的幂等键重复推送");
|
||||
if (!"EDITING".equals(plan.getPlanStatus()) || !Objects.equals(plan.getVersion(), request.expectedVersion())) throw conflict("仅编辑中的计划可开始装柜且版本必须匹配");
|
||||
ContainerBookingEntity booking = requireBooking(plan.getContainerBookingId());
|
||||
List<LoadingPlanItemEntity> items = repository.items(planId);
|
||||
if (items.isEmpty()) throw validation("装柜明细不能为空");
|
||||
checkPlanCapacity(booking, items);
|
||||
|
||||
@@ -56,6 +56,11 @@ CREATE TABLE IF NOT EXISTS fl_ocean_booking (
|
||||
OR pending_estimated_arrival_at >= pending_estimated_sailing_at)
|
||||
) ENGINE=InnoDB COMMENT='独立海运订舱聚合;柜次仍由fl_container_booking承载';
|
||||
|
||||
-- CREATE TABLE IF NOT EXISTS does not reconcile a database initialized before customer became optional.
|
||||
ALTER TABLE fl_ocean_booking
|
||||
MODIFY COLUMN customer_id BIGINT UNSIGNED NULL COMMENT '可选客户ID快照',
|
||||
MODIFY COLUMN customer_name VARCHAR(128) NULL COMMENT '可选客户名称快照';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fl_ocean_booking_party (
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '订舱提单关系方ID',
|
||||
ocean_booking_id BIGINT UNSIGNED NOT NULL COMMENT '所属海运订舱ID',
|
||||
|
||||
@@ -357,6 +357,21 @@ paths:
|
||||
responses:
|
||||
'200': {description: 成功、计划不存在或计划已进入预装流程}
|
||||
'403': {description: 无智能装柜查询权限}
|
||||
/api/platform/v1/first-leg/smart-loading/plans/{planId}/push-preload:
|
||||
post:
|
||||
summary: 推送智能装柜计划到预装清单
|
||||
description: >-
|
||||
需要 platform:firstleg:smart-loading:push 权限。真实柜号必须已录入,
|
||||
否则不得推送预装清单或创建 PDA作业任务;计划必须处于 EDITING 且版本匹配。
|
||||
parameters:
|
||||
- {name: planId, in: path, required: true, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
|
||||
- {$ref: '#/components/parameters/IdempotencyKey'}
|
||||
requestBody:
|
||||
required: true
|
||||
content: {application/json: {schema: {$ref: '#/components/schemas/SmartLoadingPushRequest'}}}
|
||||
responses:
|
||||
'200': {description: 推送成功或业务失败,按响应体 code 判断}
|
||||
'403': {description: 无智能装柜推送权限}
|
||||
/openapi/v1/inbound-measurements:
|
||||
post:
|
||||
summary: 提交一条第三方箱规测量结果和一张图片
|
||||
@@ -1273,6 +1288,11 @@ components:
|
||||
voyageNo: {type: string, maxLength: 64, nullable: true}
|
||||
version: {type: integer, minimum: 0}
|
||||
reason: {type: string, maxLength: 500}
|
||||
SmartLoadingPushRequest:
|
||||
type: object
|
||||
required: [expectedVersion]
|
||||
properties:
|
||||
expectedVersion: {type: integer, minimum: 0}
|
||||
ContainerDocumentAttachRequest:
|
||||
type: object
|
||||
required: [fileId, expectedVersion]
|
||||
|
||||
+6
@@ -26,5 +26,11 @@ class PlatformSmartLoadingControllerContractTest {
|
||||
authority("delete","platform:firstleg:smart-loading:delete",AuthenticatedUser.class,long.class,long.class,DeleteRequest.class);
|
||||
authority("push","platform:firstleg:smart-loading:push",AuthenticatedUser.class,long.class,String.class,PushRequest.class);
|
||||
}
|
||||
@Test void openApiRequiresRealContainerNumberBeforePreloadPush() throws Exception {
|
||||
String yaml=java.nio.file.Files.readString(java.nio.file.Path.of("src/main/resources/openapi/firstleg-v1.yaml"));
|
||||
assertThat(yaml).contains("/api/platform/v1/first-leg/smart-loading/plans/{planId}/push-preload:")
|
||||
.contains("真实柜号必须已录入")
|
||||
.contains("PDA作业任务");
|
||||
}
|
||||
private void authority(String method,String permission,Class<?>...types)throws Exception{Method m=PlatformSmartLoadingController.class.getDeclaredMethod(method,types);assertThat(m.getAnnotation(PreAuthorize.class).value()).isEqualTo("hasAuthority('"+permission+"')");}
|
||||
}
|
||||
|
||||
+2
@@ -20,6 +20,8 @@ class OceanBookingSqlContractTest {
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_ocean_booking_operation")
|
||||
.contains("customer_id BIGINT UNSIGNED NULL COMMENT '可选客户ID快照'")
|
||||
.contains("customer_name VARCHAR(128) NULL COMMENT '可选客户名称快照'")
|
||||
.contains("MODIFY COLUMN customer_id BIGINT UNSIGNED NULL COMMENT '可选客户ID快照'")
|
||||
.contains("MODIFY COLUMN customer_name VARCHAR(128) NULL COMMENT '可选客户名称快照'")
|
||||
.contains("service_term VARCHAR(32) NOT NULL COMMENT '运输服务条款快照'")
|
||||
.contains("freight_term VARCHAR(32) NOT NULL COMMENT '海运费支付条款快照'")
|
||||
.contains("container_booking_id BIGINT UNSIGNED NOT NULL COMMENT '明确引用现有柜次ID'")
|
||||
|
||||
@@ -387,10 +387,9 @@ class SmartLoadingServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsOverweightPlanBeforePushEvenWithoutContainerNumber() {
|
||||
void rejectsOverweightPlanBeforePush() {
|
||||
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"));
|
||||
@@ -409,21 +408,39 @@ class SmartLoadingServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void pushesWithoutContainerNumberWhenCapacityIsWithinLimit() {
|
||||
void rejectsPushWithoutContainerNumberBeforePdaOpening() {
|
||||
LoadingPlanEntity editing = plan("EDITING", 1);
|
||||
LoadingPlanEntity pushed = plan("EDITING", 2);
|
||||
ContainerBookingEntity booking = booking();
|
||||
booking.setContainerNo(null);
|
||||
when(repository.plan(1)).thenReturn(editing, pushed);
|
||||
when(repository.plan(1)).thenReturn(editing);
|
||||
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");
|
||||
BizException error = catchThrowableOfType(
|
||||
() -> service.pushPreload(1, "no-container-key", new PushRequest(1), 7, "u"),
|
||||
BizException.class);
|
||||
|
||||
assertThat(result.status()).isEqualTo("EDITING");
|
||||
verify(repository).push(1, "no-container-key", 1, 7, "u");
|
||||
verify(loadingPickService).beginIfReady(99L, 7L, "u");
|
||||
assertThat(error.getResultCode()).isEqualTo(ResultCode.VALIDATION_FAILED);
|
||||
assertThat(error.getMessage()).contains("真实柜号");
|
||||
verify(repository, never()).push(anyLong(), anyString(), anyInt(), anyLong(), anyString());
|
||||
verify(loadingPickService, never()).beginIfReady(anyLong(), anyLong(), anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsReplayWhenTheContainerNumberIsStillMissing() {
|
||||
LoadingPlanEntity pushed = plan("EDITING", 2);
|
||||
pushed.setPushIdempotencyKey("replay-key");
|
||||
ContainerBookingEntity booking = booking();
|
||||
booking.setContainerNo(" ");
|
||||
when(repository.plan(1)).thenReturn(pushed);
|
||||
when(repository.booking(99)).thenReturn(booking);
|
||||
|
||||
BizException error = catchThrowableOfType(
|
||||
() -> service.pushPreload(1, "replay-key", new PushRequest(2), 7, "u"),
|
||||
BizException.class);
|
||||
|
||||
assertThat(error.getResultCode()).isEqualTo(ResultCode.VALIDATION_FAILED);
|
||||
verify(repository, never()).push(anyLong(), anyString(), anyInt(), anyLong(), anyString());
|
||||
verify(loadingPickService, never()).beginIfReady(anyLong(), anyLong(), anyString());
|
||||
}
|
||||
|
||||
private void stubAssignment() {
|
||||
|
||||
Reference in New Issue
Block a user