优化
This commit is contained in:
+1
-2
@@ -45,12 +45,11 @@ public class PlatformLoadingPdaController {
|
||||
@GetMapping("/tasks")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:pda:view')")
|
||||
public ApiResponse<Task> task(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam(required = false) String containerNo,
|
||||
@RequestParam(required = false) String cabinetNo,
|
||||
@RequestParam(required = false) Long loadingPlanId,
|
||||
@RequestParam @Positive long warehouseId) {
|
||||
FirstlegScopeGuard.platform(user);
|
||||
return ApiResponse.success(service.task(containerNo, cabinetNo, loadingPlanId, warehouseId));
|
||||
return ApiResponse.success(service.task(cabinetNo, loadingPlanId, warehouseId));
|
||||
}
|
||||
|
||||
@GetMapping("/tasks/pending")
|
||||
|
||||
@@ -60,17 +60,10 @@ public final class LoadingPickDtos {
|
||||
@NotNull @Min(1) Long warehouseId,
|
||||
@NotNull @Min(1) Long loadingPlanId,
|
||||
@NotNull @Min(1) Long containerBookingId,
|
||||
@Size(max = 64) String containerNo,
|
||||
@NotNull @Min(0) Integer expectedVersion,
|
||||
@NotBlank @Size(max = 64) String inventorySnapshotToken,
|
||||
@NotEmpty @Size(max = 500) List<@Valid BatchItemRequest> items
|
||||
) {
|
||||
@Deprecated
|
||||
public BatchRequest(Long warehouseId, String containerNo, Integer expectedVersion,
|
||||
String inventorySnapshotToken, List<BatchItemRequest> items) {
|
||||
this(warehouseId, null, null, containerNo, expectedVersion, inventorySnapshotToken, items);
|
||||
}
|
||||
}
|
||||
) {}
|
||||
|
||||
public record BatchItemRequest(
|
||||
@NotNull @Min(1) Long itemId,
|
||||
|
||||
+4
@@ -59,6 +59,10 @@ public class OceanBookingRepository {
|
||||
public List<OceanBookingPartyEntity> parties(long id) { return parties.selectList(new LambdaQueryWrapper<OceanBookingPartyEntity>().eq(OceanBookingPartyEntity::getOceanBookingId, id).orderByAsc(OceanBookingPartyEntity::getId)); }
|
||||
public List<OceanBookingContainerRequirementEntity> requirements(long id) { return requirements.selectList(new LambdaQueryWrapper<OceanBookingContainerRequirementEntity>().eq(OceanBookingContainerRequirementEntity::getOceanBookingId, id).orderByAsc(OceanBookingContainerRequirementEntity::getSortNo)); }
|
||||
public List<OceanBookingContainerAllocationEntity> allocations(long id) { return allocations.selectList(new LambdaQueryWrapper<OceanBookingContainerAllocationEntity>().eq(OceanBookingContainerAllocationEntity::getOceanBookingId, id).orderByAsc(OceanBookingContainerAllocationEntity::getAllocatedAt)); }
|
||||
public OceanBookingContainerAllocationEntity allocationByContainer(long containerBookingId) {
|
||||
return allocations.selectOne(new LambdaQueryWrapper<OceanBookingContainerAllocationEntity>()
|
||||
.eq(OceanBookingContainerAllocationEntity::getContainerBookingId, containerBookingId));
|
||||
}
|
||||
public List<OceanBookingStatusHistoryEntity> histories(long id) { return histories.selectList(new LambdaQueryWrapper<OceanBookingStatusHistoryEntity>().eq(OceanBookingStatusHistoryEntity::getOceanBookingId, id).orderByAsc(OceanBookingStatusHistoryEntity::getCreatedAt)); }
|
||||
public void replaceParties(long id, List<OceanBookingPartyEntity> values) { parties.delete(new LambdaQueryWrapper<OceanBookingPartyEntity>().eq(OceanBookingPartyEntity::getOceanBookingId, id)); values.forEach(parties::insert); }
|
||||
public void replaceRequirements(long id, List<OceanBookingContainerRequirementEntity> values) { requirements.delete(new LambdaQueryWrapper<OceanBookingContainerRequirementEntity>().eq(OceanBookingContainerRequirementEntity::getOceanBookingId, id)); values.forEach(requirements::insert); }
|
||||
|
||||
+4
@@ -10,6 +10,7 @@ import com.baiyue.firstleg.oceanbooking.domain.OceanBookingStatus;
|
||||
import com.baiyue.firstleg.oceanbooking.dto.OceanBookingDtos.*;
|
||||
import com.baiyue.firstleg.oceanbooking.entity.*;
|
||||
import com.baiyue.firstleg.oceanbooking.repository.OceanBookingRepository;
|
||||
import com.baiyue.firstleg.service.LoadingPickService;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import jakarta.annotation.Resource;
|
||||
@@ -31,6 +32,7 @@ public class OceanBookingService {
|
||||
private static final Set<String> PARTY_TYPES = Set.of("SHIPPER", "CONSIGNEE", "NOTIFY_PARTY");
|
||||
@Resource private OceanBookingRepository repository;
|
||||
@Resource private ObjectMapper objectMapper;
|
||||
@Resource private LoadingPickService loadingPickService;
|
||||
|
||||
public PageResult<OceanBookingEntity> page(Query query) {
|
||||
String status = trim(query.status());
|
||||
@@ -106,6 +108,8 @@ public class OceanBookingService {
|
||||
requireStatus(before, OceanBookingStatus.CONFIRMED);
|
||||
if (repository.release(id, request, operatorId, name) != 1) throw concurrent();
|
||||
repository.syncReleasedTransport(id, request, operatorId, name);
|
||||
repository.allocations(id).forEach(allocation ->
|
||||
loadingPickService.beginIfReady(allocation.getContainerBookingId(), operatorId, name));
|
||||
OceanBookingEntity after = required(id);
|
||||
addHistory(id, before.getStatus(), after.getStatus(), action, request.reason(), before, after,
|
||||
operatorId, name);
|
||||
|
||||
@@ -104,23 +104,10 @@ public class ContainerService {
|
||||
history.setOperatorId(operatorId); history.setOperatorName(operatorName);
|
||||
if (transportHistories != null) transportHistories.insert(history);
|
||||
ContainerBookingEntity updated = required(id);
|
||||
beginPdaTaskAfterContainerBound(updated, containerNo, operatorId, operatorName);
|
||||
loadingPickService.beginIfReady(updated.getId(), operatorId, operatorName);
|
||||
return updated;
|
||||
}
|
||||
|
||||
/** 预装清单先推送、后录入集装箱号时,补建对应的 PDA 拣货任务。 */
|
||||
private void beginPdaTaskAfterContainerBound(ContainerBookingEntity booking, String containerNo,
|
||||
long operatorId, String operatorName) {
|
||||
if (containerNo == null) return;
|
||||
LoadingPlanEntity plan = smartLoadingRepository.planByBooking(booking.getId());
|
||||
if (plan == null || plan.getPreloadPushedAt() == null || !"EDITING".equals(plan.getPlanStatus())) return;
|
||||
// The PDA task and its item versions are stable work identities. A later transport-number
|
||||
// correction only changes the booking display value and must not resynchronize the task.
|
||||
if (loadingPickRepository.taskByPlan(plan.getId()) != null) return;
|
||||
loadingPickService.begin(plan.getId(), containerNo, operatorId, operatorName,
|
||||
smartLoadingRepository.items(plan.getId()));
|
||||
}
|
||||
|
||||
private ContainerBookingEntity required(long id) {
|
||||
ContainerBookingEntity value = repository.find(id);
|
||||
if (value == null) throw new BizException(ResultCode.NOT_FOUND, "货柜不存在");
|
||||
|
||||
@@ -174,8 +174,12 @@ public class InboundMeasurementService {
|
||||
InboundOrderEntity inbound = requireInbound(tenantId, inboundId);
|
||||
InboundWeightResultEntity weight = requireWeight(tenantId, inboundId, resultId);
|
||||
requireVersion(request.version(), weight.getVersion());
|
||||
boolean wasConfirmed = WEIGHT_CONFIRMED.equals(weight.getRecognitionStatus());
|
||||
applyWeightCorrection(weight, request.version(), request.adjustedWeightValue(),
|
||||
request.adjustedWeightUnit(), request.adjustmentReason(), operatorId, operatorName);
|
||||
if (wasConfirmed) {
|
||||
recalculateConfirmedWeight(inbound, operatorId);
|
||||
}
|
||||
return toWeight(weight);
|
||||
}
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ import com.baiyue.firstleg.entity.LoadingPlanHistoryEntity;
|
||||
import com.baiyue.firstleg.mapper.LoadingPickItemMapper.CartonLabelRow;
|
||||
import com.baiyue.firstleg.repository.LoadingPickRepository;
|
||||
import com.baiyue.firstleg.repository.SmartLoadingRepository;
|
||||
import com.baiyue.firstleg.oceanbooking.repository.OceanBookingRepository;
|
||||
import jakarta.annotation.Resource;
|
||||
import feign.FeignException;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
@@ -65,6 +66,8 @@ public class LoadingPickService {
|
||||
@Resource
|
||||
private SmartLoadingRepository smartLoadingRepository;
|
||||
@Resource
|
||||
private OceanBookingRepository oceanBookingRepository;
|
||||
@Resource
|
||||
private LoadingListService loadingListService;
|
||||
@Resource
|
||||
private PdaPickSettlementService settlementService;
|
||||
@@ -73,6 +76,22 @@ public class LoadingPickService {
|
||||
@Value("${baiyue.security.internal-service-token:test-internal-service-token}")
|
||||
private String internalToken;
|
||||
|
||||
/** Opens the field task only after planning, transport identity and ocean release have converged. */
|
||||
@Transactional
|
||||
public void beginIfReady(long containerBookingId, long operatorId, String operatorName) {
|
||||
var booking = smartLoadingRepository.booking(containerBookingId);
|
||||
if (booking == null || !StringUtils.hasText(booking.getContainerNo())) return;
|
||||
var plan = smartLoadingRepository.planByBooking(containerBookingId);
|
||||
if (plan == null || plan.getPreloadPushedAt() == null || !"EDITING".equals(plan.getPlanStatus())) return;
|
||||
var allocation = oceanBookingRepository.allocationByContainer(containerBookingId);
|
||||
if (allocation == null) return;
|
||||
var oceanBooking = oceanBookingRepository.find(allocation.getOceanBookingId());
|
||||
if (oceanBooking == null || !"RELEASED".equals(oceanBooking.getStatus())) return;
|
||||
if (repository.taskByPlan(plan.getId()) != null) return;
|
||||
begin(plan.getId(), booking.getContainerNo(), operatorId, operatorName,
|
||||
smartLoadingRepository.items(plan.getId()));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void begin(long planId, String containerNo, long operatorId, String operatorName,
|
||||
List<LoadingPlanItemEntity> planItems) {
|
||||
@@ -111,29 +130,14 @@ public class LoadingPickService {
|
||||
}
|
||||
}
|
||||
|
||||
public Task task(String containerNo, long warehouseId) {
|
||||
if (!StringUtils.hasText(containerNo)) {
|
||||
throw new BizException(ResultCode.BAD_REQUEST, "集装箱号不能为空");
|
||||
}
|
||||
if (warehouseId <= 0) {
|
||||
throw new BizException(ResultCode.BAD_REQUEST, "warehouseId必须大于0");
|
||||
}
|
||||
LoadingPickTaskEntity task = repository.taskByContainer(containerNo.trim(), warehouseId);
|
||||
if (task == null) {
|
||||
throw new BizException(ResultCode.NOT_FOUND, "未找到装柜中的PDA拣货任务");
|
||||
}
|
||||
return view(task, warehouseId);
|
||||
}
|
||||
|
||||
public Task task(String containerNo, String cabinetNo, Long loadingPlanId, long warehouseId) {
|
||||
int selectors = (StringUtils.hasText(containerNo) ? 1 : 0) + (StringUtils.hasText(cabinetNo) ? 1 : 0)
|
||||
public Task task(String cabinetNo, Long loadingPlanId, long warehouseId) {
|
||||
int selectors = (StringUtils.hasText(cabinetNo) ? 1 : 0)
|
||||
+ (loadingPlanId != null ? 1 : 0);
|
||||
if (selectors != 1) throw new BizException(ResultCode.BAD_REQUEST,
|
||||
"containerNo、cabinetNo、loadingPlanId必须且只能提供一个");
|
||||
"cabinetNo、loadingPlanId必须且只能提供一个");
|
||||
LoadingPickTaskEntity task = StringUtils.hasText(cabinetNo)
|
||||
? repository.taskByCabinet(cabinetNo.trim(), warehouseId)
|
||||
: loadingPlanId != null ? repository.taskByPlan(loadingPlanId, warehouseId)
|
||||
: repository.taskByContainer(containerNo.trim(), warehouseId);
|
||||
: repository.taskByPlan(loadingPlanId, warehouseId);
|
||||
if (task == null) throw new BizException(ResultCode.NOT_FOUND, "未找到装柜中的PDA拣货任务");
|
||||
return view(task, warehouseId);
|
||||
}
|
||||
@@ -854,6 +858,7 @@ public class LoadingPickService {
|
||||
}
|
||||
StringBuilder canonical = new StringBuilder("BATCH|").append(taskId).append('|')
|
||||
.append(request.warehouseId()).append('|').append(request.loadingPlanId()).append('|')
|
||||
.append(request.containerBookingId()).append('|')
|
||||
.append(request.expectedVersion()).append('|').append(request.inventorySnapshotToken().trim());
|
||||
request.items().stream().sorted(Comparator.comparing(BatchItemRequest::itemId,
|
||||
Comparator.nullsFirst(Long::compareTo)))
|
||||
|
||||
@@ -49,7 +49,7 @@ public class MerchantPriceBookService {
|
||||
}
|
||||
|
||||
public Response requireAvailableChannel(long merchantTenantId, long originWarehouseId, String channelCode,
|
||||
String destinationType) {
|
||||
String destinationType) {
|
||||
requireMerchant(merchantTenantId);
|
||||
if (originWarehouseId <= 0) throw validation("请选择有效起运仓");
|
||||
Long versionId = repository.activeVersionId();
|
||||
@@ -59,7 +59,7 @@ public class MerchantPriceBookService {
|
||||
}
|
||||
var prefix = repository.channelPrefix(normalized);
|
||||
if (prefix == null || !StringUtils.hasText(prefix.inboundCodePrefix())) {
|
||||
throw new BizException(ResultCode.CONFLICT, "所选渠道未配置入仓码前缀");
|
||||
throw new BizException(ResultCode.CONFLICT, "所选渠道未配置进仓编码");
|
||||
}
|
||||
String selected = "OVERSEAS".equals(destinationType) && StringUtils.hasText(prefix.overseasInboundCodePrefix())
|
||||
? prefix.overseasInboundCodePrefix() : prefix.inboundCodePrefix();
|
||||
|
||||
@@ -225,21 +225,22 @@ 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, "装柜计划不存在");
|
||||
if (key.equals(plan.getPushIdempotencyKey())) return view(plan, requireBooking(plan.getContainerBookingId()));
|
||||
if (key.equals(plan.getPushIdempotencyKey())) {
|
||||
loadingPickService.beginIfReady(plan.getContainerBookingId(), operatorId, operatorName);
|
||||
return view(plan, requireBooking(plan.getContainerBookingId()));
|
||||
}
|
||||
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);
|
||||
if (StringUtils.hasText(booking.getContainerNo())) {
|
||||
loadingPickService.begin(planId, booking.getContainerNo(), operatorId, operatorName, items);
|
||||
}
|
||||
try {
|
||||
if (repository.push(planId, key, request.expectedVersion(), operatorId, operatorName) != 1) throw conflict("装柜计划已被并发修改");
|
||||
} catch (DuplicateKeyException e) { throw conflict("幂等键已用于其他推送"); }
|
||||
history(planId, null, "PUSH_PRELOAD", "EDITING", "EDITING", key, operatorId, operatorName);
|
||||
LoadingPlanEntity pushed = repository.plan(planId);
|
||||
loadingPickService.beginIfReady(pushed.getContainerBookingId(), operatorId, operatorName);
|
||||
return view(pushed, requireBooking(pushed.getContainerBookingId()));
|
||||
}
|
||||
|
||||
|
||||
@@ -421,8 +421,8 @@ CREATE TABLE IF NOT EXISTS fl_price_channel_catalog (
|
||||
price_model VARCHAR(32) NOT NULL COMMENT '默认价格模型',
|
||||
source_sheet_name VARCHAR(128) NOT NULL COMMENT '来源Sheet名称;海派为同一Sheet',
|
||||
aliases_json JSON NOT NULL COMMENT '上传名称别名数组',
|
||||
inbound_code_prefix VARCHAR(6) NOT NULL COMMENT '普通目的仓入仓码物流前缀',
|
||||
overseas_inbound_code_prefix VARCHAR(6) NULL COMMENT '海外仓中转入仓码物流前缀;为空时使用普通前缀',
|
||||
inbound_code_prefix VARCHAR(6) NOT NULL COMMENT '普通目的仓进仓编码前缀;包税与不包税共用',
|
||||
overseas_inbound_code_prefix VARCHAR(6) NULL COMMENT '海外仓中转进仓编码前缀;包税与不包税共用,为空时使用普通前缀',
|
||||
status VARCHAR(16) NOT NULL DEFAULT 'ACTIVE' COMMENT '目录状态:ACTIVE启用、DISABLED停用',
|
||||
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 '最后更新时间',
|
||||
|
||||
@@ -49,6 +49,26 @@ paths:
|
||||
- {name: initializationKey, in: query, required: true, schema: {type: string, minLength: 1, maxLength: 128}}
|
||||
responses:
|
||||
'200': {description: 幂等补偿完成}
|
||||
/internal/firstleg/v1/pricing/channels/validate:
|
||||
post:
|
||||
summary: 校验订单渠道并返回进仓编码前缀
|
||||
description: 仅供订单服务调用;前缀按渠道和目的仓类型选择,同一渠道的包税与不包税订单共用编码前缀。
|
||||
security:
|
||||
- internalServiceToken: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required: [tenantId, originWarehouseId, channelCode, destinationType]
|
||||
properties:
|
||||
tenantId: {type: string, pattern: '^[1-9][0-9]*$'}
|
||||
originWarehouseId: {type: string, pattern: '^[1-9][0-9]*$'}
|
||||
channelCode: {type: string, minLength: 1, maxLength: 64}
|
||||
destinationType: {type: string, enum: [FBA, OVERSEAS, PRIVATE]}
|
||||
responses:
|
||||
'200': {description: 成功时 data 返回 channelCode 和 inboundCodePrefix;未配置对应进仓编码时返回业务冲突}
|
||||
/api/platform/v1/first-leg/customer-dimension-config:
|
||||
get:
|
||||
summary: 查询客户固定四箱规尺寸配置
|
||||
|
||||
@@ -7,7 +7,6 @@ paths:
|
||||
get:
|
||||
summary: Find the active container picking task in one platform warehouse
|
||||
parameters:
|
||||
- {name: containerNo, in: query, required: false, deprecated: true, description: Compatibility lookup only; taskId is the stable identity, schema: {type: string}}
|
||||
- {name: cabinetNo, in: query, required: false, schema: {type: string}}
|
||||
- {name: loadingPlanId, in: query, required: false, schema: {type: integer, format: int64}}
|
||||
- {name: warehouseId, in: query, required: true, schema: {type: integer, format: int64, minimum: 1}}
|
||||
@@ -195,7 +194,6 @@ components:
|
||||
warehouseId: {type: integer, format: int64, minimum: 1}
|
||||
loadingPlanId: {type: integer, format: int64, minimum: 1}
|
||||
containerBookingId: {type: integer, format: int64, minimum: 1}
|
||||
containerNo: {type: string, maxLength: 64, nullable: true, deprecated: true, description: Accepted during compatibility period but ignored for identity and idempotency}
|
||||
expectedVersion: {type: integer, minimum: 0}
|
||||
inventorySnapshotToken: {type: string, minLength: 64, maxLength: 64}
|
||||
items:
|
||||
|
||||
+13
@@ -31,5 +31,18 @@ class PdaConvergenceOpenApiContractTest {
|
||||
.contains("changing it never resynchronizes the task or item versions")
|
||||
.contains("expectedTaskVersion")
|
||||
.contains("CompleteLoadingRequest");
|
||||
|
||||
String taskLookup = yaml.substring(
|
||||
yaml.indexOf(" /api/platform/v1/first-leg/pda-loading/tasks:"),
|
||||
yaml.indexOf(" /api/platform/v1/first-leg/pda-loading/tasks/pending:"));
|
||||
String batchRequest = yaml.substring(
|
||||
yaml.indexOf(" BatchRequest:"),
|
||||
yaml.indexOf(" BatchItemRequest:"));
|
||||
assertThat(taskLookup)
|
||||
.contains("name: cabinetNo", "name: loadingPlanId")
|
||||
.doesNotContain("name: containerNo");
|
||||
assertThat(batchRequest)
|
||||
.contains("loadingPlanId:", "containerBookingId:")
|
||||
.doesNotContain("containerNo:");
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
class PlatformLoadingPdaControllerContractTest {
|
||||
@Test
|
||||
void exposesAssignmentWorkAndReviewWithDedicatedAuthorities() throws Exception {
|
||||
authority("task", "platform:firstleg:pda:view", AuthenticatedUser.class, String.class, String.class, Long.class, long.class);
|
||||
authority("task", "platform:firstleg:pda:view", AuthenticatedUser.class, String.class, Long.class, long.class);
|
||||
authority("pendingTasks", "platform:firstleg:pda:view", AuthenticatedUser.class, String.class);
|
||||
authority("inventorySnapshot", "platform:firstleg:pda:view", AuthenticatedUser.class, long.class, long.class);
|
||||
authority("claim", "platform:firstleg:pda:work", AuthenticatedUser.class, long.class, ClaimRequest.class);
|
||||
|
||||
@@ -16,6 +16,7 @@ import java.nio.charset.StandardCharsets;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.groups.Tuple.tuple;
|
||||
|
||||
@DisplayName("船期价格表公共契约")
|
||||
class PriceBookContractTest {
|
||||
@@ -24,11 +25,23 @@ class PriceBookContractTest {
|
||||
void keepsStableChannelCatalog() {
|
||||
assertThat(PriceBookChannelCatalog.CHANNELS).hasSize(15);
|
||||
assertThat(PriceBookChannelCatalog.CHANNELS)
|
||||
.extracting(value -> value.channelCode() + ":" + value.inboundCodePrefix())
|
||||
.contains("MATSON_CLX_TRUCK:ZBYHK", "OA_LA_FULL_TRUCK:PCKK", "OA_CHICAGO_RAIL:CPHT");
|
||||
assertThat(PriceBookChannelCatalog.CHANNELS)
|
||||
.filteredOn(value -> "OA_NEW_YORK_TRUCK".equals(value.channelCode()))
|
||||
.singleElement().extracting(value -> value.overseasInboundCodePrefix()).isEqualTo("NPHWK");
|
||||
.extracting("channelCode", "inboundCodePrefix", "overseasInboundCodePrefix")
|
||||
.containsExactly(
|
||||
tuple("MATSON_CLX_TRUCK", "ZBYHK", "ZBYHWK"),
|
||||
tuple("MATSON_MAX_TRUCK", "BYHK", "BYHWK"),
|
||||
tuple("EXX_TRUCK", "EXXHK", "EXXHWK"),
|
||||
tuple("HEDE_TRUCK", "HDHK", "HDHWK"),
|
||||
tuple("EXX_RAIL_NY", "NYEXX", null),
|
||||
tuple("OA_LA_TRUCK", "PCHK", "PCHWK"),
|
||||
tuple("OA_LA_FULL_TRUCK", "PCKK", null),
|
||||
tuple("OA_CHICAGO_RAIL", "CPHT", null),
|
||||
tuple("OA_HOUSTON_TRUCK", "HPHK", null),
|
||||
tuple("OA_NEW_YORK_TRUCK", "NPHK", "NPHWK"),
|
||||
tuple("OA_SAVANNAH_TRUCK", "SPHK", null),
|
||||
tuple("MATSON_CLX_PARCEL", "ZBYHP", null),
|
||||
tuple("MATSON_MAX_PARCEL", "BYHP", null),
|
||||
tuple("HEDE_PARCEL", "HDHP", null),
|
||||
tuple("OA_PARCEL", "PCHP", null));
|
||||
assertThat(PriceBookChannelCatalog.CHANNELS.stream()
|
||||
.map(PriceBookChannelCatalog.ChannelDefinition::channelCode))
|
||||
.containsExactly(
|
||||
@@ -102,5 +115,12 @@ class PriceBookContractTest {
|
||||
.contains("/api/platform/v1/fba-pricing/carrier-rates/price-books/{versionId}/source")
|
||||
.contains("确认并立即生效")
|
||||
.contains("SUPERSEDED");
|
||||
|
||||
String firstlegOpenapi = new ClassPathResource("openapi/firstleg-v1.yaml")
|
||||
.getContentAsString(StandardCharsets.UTF_8);
|
||||
assertThat(firstlegOpenapi)
|
||||
.contains("/internal/firstleg/v1/pricing/channels/validate:")
|
||||
.contains("required: [tenantId, originWarehouseId, channelCode, destinationType]")
|
||||
.contains("同一渠道的包税与不包税订单共用编码前缀");
|
||||
}
|
||||
}
|
||||
|
||||
+21
@@ -5,6 +5,8 @@ import com.baiyue.firstleg.oceanbooking.dto.OceanBookingDtos.ActionRequest;
|
||||
import com.baiyue.firstleg.oceanbooking.dto.OceanBookingDtos.ChangeRequest;
|
||||
import com.baiyue.firstleg.oceanbooking.dto.OceanBookingDtos.Detail;
|
||||
import com.baiyue.firstleg.oceanbooking.dto.OceanBookingDtos.AllocationRequest;
|
||||
import com.baiyue.firstleg.oceanbooking.dto.OceanBookingDtos.ReleaseRequest;
|
||||
import com.baiyue.firstleg.service.LoadingPickService;
|
||||
import com.baiyue.firstleg.entity.ContainerBookingEntity;
|
||||
import com.baiyue.firstleg.oceanbooking.entity.OceanBookingContainerAllocationEntity;
|
||||
import com.baiyue.firstleg.oceanbooking.entity.OceanBookingContainerRequirementEntity;
|
||||
@@ -30,6 +32,7 @@ import static org.mockito.Mockito.*;
|
||||
|
||||
class OceanBookingServiceTest {
|
||||
@Mock private OceanBookingRepository repository;
|
||||
@Mock private LoadingPickService loadingPickService;
|
||||
@InjectMocks private OceanBookingService service;
|
||||
|
||||
@BeforeEach
|
||||
@@ -77,6 +80,24 @@ class OceanBookingServiceTest {
|
||||
verify(repository, never()).transition(anyLong(), anyString(), anyString(), any(), any(), anyInt(), anyLong(), anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void opensAllocatedContainerPdaTasksAfterRelease() {
|
||||
OceanBookingEntity confirmed = booking("CONFIRMED", 3);
|
||||
OceanBookingEntity released = booking("RELEASED", 4);
|
||||
var allocation = new OceanBookingContainerAllocationEntity();
|
||||
allocation.setContainerBookingId(12L);
|
||||
when(repository.find(1L)).thenReturn(confirmed, released);
|
||||
when(repository.release(eq(1L), any(ReleaseRequest.class), eq(9L), eq("admin"))).thenReturn(1);
|
||||
when(repository.allocations(1L)).thenReturn(List.of(allocation));
|
||||
var request = new ReleaseRequest(3, "BL-001", "VESSEL", "V001",
|
||||
LocalDateTime.of(2026, 9, 1, 0, 0), LocalDateTime.of(2026, 9, 20, 0, 0), "放舱");
|
||||
|
||||
Detail result = service.release(1L, 9L, "admin", "release-1", request);
|
||||
|
||||
assertThat(result.oceanBooking().getStatus()).isEqualTo("RELEASED");
|
||||
verify(loadingPickService).beginIfReady(12L, 9L, "admin");
|
||||
}
|
||||
|
||||
@Test
|
||||
void keepsCurrentVoyagePendingUntilChangeConfirmation() {
|
||||
OceanBookingEntity current = booking("CONFIRMED", 5);
|
||||
|
||||
@@ -143,9 +143,7 @@ class ContainerServiceTest {
|
||||
service.updateTransport(11L, 9L, "admin",
|
||||
new TransportUpdateRequest("MSCU1234567", "V002", 2));
|
||||
|
||||
verify(loadingPickService).begin(org.mockito.ArgumentMatchers.eq(31L),
|
||||
org.mockito.ArgumentMatchers.eq("MSCU1234567"), org.mockito.ArgumentMatchers.eq(9L),
|
||||
org.mockito.ArgumentMatchers.eq("admin"), any());
|
||||
verify(loadingPickService).beginIfReady(11L, 9L, "admin");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -165,8 +163,7 @@ class ContainerServiceTest {
|
||||
service.updateTransport(11L, 9L, "admin",
|
||||
new TransportUpdateRequest("MSCU7654321", "V002", 2));
|
||||
|
||||
verify(loadingPickService, never()).begin(anyLong(), any(), anyLong(), any(), any());
|
||||
verify(smartLoadingRepository, never()).items(31L);
|
||||
verify(loadingPickService).beginIfReady(11L, 9L, "admin");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -85,6 +85,28 @@ class InboundMeasurementServiceTest {
|
||||
assertThat(revision.getValue().getAfterWeightValue()).isEqualByComparingTo("13.200");
|
||||
}
|
||||
|
||||
@Test
|
||||
void correctingConfirmedWeightImmediatelyRemovesItFromInboundTotal() {
|
||||
InboundOrderEntity inbound = inbound("MEASURING");
|
||||
inbound.setActualGrossWeightKg(new BigDecimal("12.500"));
|
||||
InboundWeightResultEntity weight = weight(20L, "12.500", "KG", "CONFIRMED", 4);
|
||||
weight.setFinalWeightKg(new BigDecimal("12.500"));
|
||||
when(repository.findInbound(1L, 10L)).thenReturn(inbound);
|
||||
when(repository.findWeight(1L, 10L, 20L)).thenReturn(weight);
|
||||
when(repository.updateWeight(weight)).thenReturn(true);
|
||||
when(repository.listWeights(1L, 10L)).thenReturn(List.of(weight));
|
||||
when(repository.updateInboundWeightSummary(inbound)).thenReturn(true);
|
||||
|
||||
service.correctWeightResult(1L, 10L, 20L, 9L, "操作员",
|
||||
new InboundMeasurementRequests.CorrectWeightResultRequest(
|
||||
4, new BigDecimal("13.200"), "KG", "秤面显示修正"));
|
||||
|
||||
assertThat(weight.getRecognitionStatus()).isEqualTo("PENDING_CONFIRM");
|
||||
assertThat(weight.getFinalWeightKg()).isNull();
|
||||
assertThat(inbound.getActualGrossWeightKg()).isNull();
|
||||
verify(repository).updateInboundWeightSummary(inbound);
|
||||
}
|
||||
|
||||
@Test
|
||||
void detailReturnsStandardPalletRecommendationWithoutAllocatingPallet() {
|
||||
InboundOrderEntity inbound = inbound("MEASURE_PENDING");
|
||||
|
||||
@@ -19,6 +19,9 @@ import com.baiyue.firstleg.entity.LoadingPickTaskEntity;
|
||||
import com.baiyue.firstleg.entity.LoadingPlanItemEntity;
|
||||
import com.baiyue.firstleg.repository.LoadingPickRepository;
|
||||
import com.baiyue.firstleg.repository.SmartLoadingRepository;
|
||||
import com.baiyue.firstleg.oceanbooking.repository.OceanBookingRepository;
|
||||
import com.baiyue.firstleg.oceanbooking.entity.OceanBookingContainerAllocationEntity;
|
||||
import com.baiyue.firstleg.oceanbooking.entity.OceanBookingEntity;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.InOrder;
|
||||
@@ -46,6 +49,8 @@ class LoadingPickServiceTest {
|
||||
@MockBean
|
||||
private SmartLoadingRepository smartLoadingRepository;
|
||||
@MockBean
|
||||
private OceanBookingRepository oceanBookingRepository;
|
||||
@MockBean
|
||||
private LoadingListService loadingListService;
|
||||
@MockBean
|
||||
private PdaPickSettlementService settlementService;
|
||||
@@ -54,6 +59,51 @@ class LoadingPickServiceTest {
|
||||
@Resource
|
||||
private LoadingPickService service;
|
||||
|
||||
@Test
|
||||
void opensTaskOnlyAfterReleasedBookingAndRealContainerNumber() {
|
||||
var booking = new com.baiyue.firstleg.entity.ContainerBookingEntity();
|
||||
booking.setId(40L); booking.setContainerNo("MSCU-001");
|
||||
var plan = new com.baiyue.firstleg.entity.LoadingPlanEntity();
|
||||
plan.setId(10L); plan.setContainerBookingId(40L); plan.setPlanStatus("EDITING");
|
||||
plan.setPreloadPushedAt(java.time.LocalDateTime.now());
|
||||
var allocation = new OceanBookingContainerAllocationEntity(); allocation.setOceanBookingId(90L);
|
||||
var ocean = new OceanBookingEntity(); ocean.setId(90L); ocean.setStatus("RELEASED");
|
||||
when(smartLoadingRepository.booking(40L)).thenReturn(booking);
|
||||
when(smartLoadingRepository.planByBooking(40L)).thenReturn(plan);
|
||||
when(smartLoadingRepository.plan(10L)).thenReturn(plan);
|
||||
when(oceanBookingRepository.allocationByContainer(40L)).thenReturn(allocation);
|
||||
when(oceanBookingRepository.find(90L)).thenReturn(ocean);
|
||||
when(repository.taskByPlan(10L)).thenReturn(null);
|
||||
when(smartLoadingRepository.items(10L)).thenReturn(List.of());
|
||||
doAnswer(invocation -> {
|
||||
invocation.getArgument(0, LoadingPickTaskEntity.class).setId(20L);
|
||||
return null;
|
||||
}).when(repository).insert(any(LoadingPickTaskEntity.class));
|
||||
|
||||
service.beginIfReady(40L, 7L, "admin");
|
||||
|
||||
verify(repository).insert(any(LoadingPickTaskEntity.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void doesNotOpenTaskBeforeOceanRelease() {
|
||||
var booking = new com.baiyue.firstleg.entity.ContainerBookingEntity();
|
||||
booking.setId(40L); booking.setContainerNo("MSCU-001");
|
||||
var plan = new com.baiyue.firstleg.entity.LoadingPlanEntity();
|
||||
plan.setId(10L); plan.setContainerBookingId(40L); plan.setPlanStatus("EDITING");
|
||||
plan.setPreloadPushedAt(java.time.LocalDateTime.now());
|
||||
var allocation = new OceanBookingContainerAllocationEntity(); allocation.setOceanBookingId(90L);
|
||||
var ocean = new OceanBookingEntity(); ocean.setId(90L); ocean.setStatus("CONFIRMED");
|
||||
when(smartLoadingRepository.booking(40L)).thenReturn(booking);
|
||||
when(smartLoadingRepository.planByBooking(40L)).thenReturn(plan);
|
||||
when(oceanBookingRepository.allocationByContainer(40L)).thenReturn(allocation);
|
||||
when(oceanBookingRepository.find(90L)).thenReturn(ocean);
|
||||
|
||||
service.beginIfReady(40L, 7L, "admin");
|
||||
|
||||
verify(repository, never()).insert(any(LoadingPickTaskEntity.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void stableCabinetAndPlanQueriesReturnSameTaskWithCurrentContainerNumber() {
|
||||
LoadingPickTaskEntity task = task();
|
||||
@@ -66,8 +116,8 @@ class LoadingPickServiceTest {
|
||||
when(repository.items(20L, 101L)).thenReturn(List.of());
|
||||
when(repository.cartonLabels(20L)).thenReturn(List.of());
|
||||
|
||||
var byCabinet = service.task(null, "CAB-001", null, 101L);
|
||||
var byPlan = service.task(null, null, 10L, 101L);
|
||||
var byCabinet = service.task("CAB-001", null, 101L);
|
||||
var byPlan = service.task(null, 10L, 101L);
|
||||
|
||||
assertThat(byCabinet.id()).isEqualTo(20L);
|
||||
assertThat(byPlan.id()).isEqualTo(20L);
|
||||
@@ -76,27 +126,12 @@ class LoadingPickServiceTest {
|
||||
|
||||
@Test
|
||||
void rejectsMissingOrConflictingCompatibilitySelectors() {
|
||||
assertThatThrownBy(() -> service.task(null, null, null, 101L))
|
||||
assertThatThrownBy(() -> service.task(null, null, 101L))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("必须且只能提供一个");
|
||||
assertThatThrownBy(() -> service.task("MSCU", "CAB", null, 101L))
|
||||
assertThatThrownBy(() -> service.task("CAB", 10L, 101L))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("必须且只能提供一个");
|
||||
}
|
||||
|
||||
@Test
|
||||
void legacyContainerLookupReturnsCurrentTransportDisplayWithoutChangingIdentity() {
|
||||
LoadingPickTaskEntity task = task(); task.setContainerBookingId(40L); task.setCabinetNo("CAB-001");
|
||||
var booking = new com.baiyue.firstleg.entity.ContainerBookingEntity();
|
||||
booking.setId(40L); booking.setContainerNo("MSCU-CURRENT");
|
||||
when(repository.taskByContainer("MSCU-CURRENT", 101L)).thenReturn(task);
|
||||
when(smartLoadingRepository.booking(40L)).thenReturn(booking);
|
||||
when(repository.items(20L, 101L)).thenReturn(List.of());
|
||||
when(repository.cartonLabels(20L)).thenReturn(List.of());
|
||||
var result = service.task("MSCU-CURRENT", null, null, 101L);
|
||||
assertThat(result.id()).isEqualTo(20L);
|
||||
assertThat(result.loadingPlanId()).isEqualTo(10L);
|
||||
assertThat(result.currentContainerNo()).isEqualTo("MSCU-CURRENT");
|
||||
}
|
||||
|
||||
@Test
|
||||
void keepsPlanLoadingWhenItemsRemain() {
|
||||
LoadingPickTaskEntity task = task();
|
||||
@@ -271,7 +306,7 @@ class LoadingPickServiceTest {
|
||||
ApiResponse.success(List.of(new WarehousePalletLocationClient.OccupiedResource(
|
||||
71L, 72L, 0, 101L, 73L, 74L, "A-01", false, 2))));
|
||||
String snapshotToken = service.inventorySnapshot(20L, 101L).snapshotToken();
|
||||
BatchRequest request = new BatchRequest(101L, 10L, 99L, "CSLU1234567", 0, snapshotToken, List.of(
|
||||
BatchRequest request = new BatchRequest(101L, 10L, 99L, 0, snapshotToken, List.of(
|
||||
new BatchItemRequest(30L, "PICK", 2, null, "MANUAL", List.of(), 0, null),
|
||||
new BatchItemRequest(31L, "SKIP", null, null, null, null, 0, "damaged carton")));
|
||||
|
||||
@@ -301,7 +336,7 @@ class LoadingPickServiceTest {
|
||||
when(repository.assignment(20L)).thenReturn(assignment());
|
||||
when(repository.items(20L)).thenReturn(items);
|
||||
when(repository.items(20L, 101L)).thenReturn(items);
|
||||
BatchRequest request = new BatchRequest(101L, 10L, 99L, "CSLU1234567", 0, "stale-token", List.of(
|
||||
BatchRequest request = new BatchRequest(101L, 10L, 99L, 0, "stale-token", List.of(
|
||||
new BatchItemRequest(30L, "PICK", 2, null, "MANUAL", List.of(), 0, null),
|
||||
new BatchItemRequest(31L, "PARTIAL", null, 4, "AUTO", null, 0, null)));
|
||||
|
||||
@@ -325,7 +360,7 @@ class LoadingPickServiceTest {
|
||||
when(warehouse.occupiedResources(any(), any(Long.class), any(), any(Long.class))).thenReturn(
|
||||
ApiResponse.success(List.of(new WarehousePalletLocationClient.OccupiedResource(
|
||||
71L, 72L, 0, 101L, 73L, 74L, "A-01", false, 2))));
|
||||
BatchRequest request = new BatchRequest(101L, 10L, 99L, "CSLU1234567", 0, "0".repeat(64), List.of(
|
||||
BatchRequest request = new BatchRequest(101L, 10L, 99L, 0, "0".repeat(64), List.of(
|
||||
new BatchItemRequest(30L, "PICK", 2, null, "MANUAL", List.of(), 0, null)));
|
||||
|
||||
assertThatThrownBy(() -> service.batch(20L, "batch-inventory-stale", request, 9L, "operator"))
|
||||
@@ -343,7 +378,7 @@ class LoadingPickServiceTest {
|
||||
LoadingPickTaskEntity task = task();
|
||||
task.setVersion(3);
|
||||
when(repository.taskForUpdate(20L)).thenReturn(task);
|
||||
BatchRequest request = new BatchRequest(101L, 10L, 99L, "CSLU1234567", 2, "stale-token", List.of(
|
||||
BatchRequest request = new BatchRequest(101L, 10L, 99L, 2, "stale-token", List.of(
|
||||
new BatchItemRequest(30L, "PICK", 2, null, "AUTO", null, 0, null)));
|
||||
|
||||
assertThatThrownBy(() -> service.batch(20L, "batch-stale", request, 9L, "operator"))
|
||||
@@ -373,14 +408,14 @@ class LoadingPickServiceTest {
|
||||
30L, "PICK", 2, null, "MANUAL", List.of(), 0, null);
|
||||
|
||||
assertThatThrownBy(() -> service.batch(20L, "batch-stale", new BatchRequest(
|
||||
101L, 10L, 99L, "CSLU1234567", 1, snapshotToken, List.of(line)), 9L, "operator"))
|
||||
101L, 10L, 99L, 1, snapshotToken, List.of(line)), 9L, "operator"))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessageContaining("任务版本已变化");
|
||||
|
||||
assertThat(item.getVersion()).isZero();
|
||||
assertThat(item.getPickedPackageCount()).isZero();
|
||||
service.batch(20L, "batch-valid", new BatchRequest(
|
||||
101L, 10L, 99L, "CSLU1234567", 0, snapshotToken, List.of(line)), 9L, "operator");
|
||||
101L, 10L, 99L, 0, snapshotToken, List.of(line)), 9L, "operator");
|
||||
|
||||
verify(repository, times(1)).pick(30L, 2, "MANUAL", "[]", 0, 101L);
|
||||
verify(repository, times(1)).touchTask(20L, 0);
|
||||
@@ -401,7 +436,7 @@ class LoadingPickServiceTest {
|
||||
|
||||
service.begin(10L, "CSLU1234567", 9L, "operator", List.of(planItem));
|
||||
|
||||
BatchRequest staleRequest = new BatchRequest(101L, 10L, 99L, "CSLU1234567", 1,
|
||||
BatchRequest staleRequest = new BatchRequest(101L, 10L, 99L, 1,
|
||||
"stale-token", List.of(
|
||||
new BatchItemRequest(30L, "PICK", 2, null, "AUTO", null, 0, null)));
|
||||
assertThatThrownBy(() -> service.batch(20L, "batch-stale-after-resync", staleRequest,
|
||||
|
||||
@@ -389,7 +389,7 @@ class SmartLoadingServiceTest {
|
||||
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());
|
||||
verify(loadingPickService, never()).beginIfReady(anyLong(), anyLong(), anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -407,7 +407,7 @@ class SmartLoadingServiceTest {
|
||||
|
||||
assertThat(result.status()).isEqualTo("EDITING");
|
||||
verify(repository).push(1, "no-container-key", 1, 7, "u");
|
||||
verify(loadingPickService, never()).begin(anyLong(), any(), anyLong(), anyString(), anyList());
|
||||
verify(loadingPickService).beginIfReady(99L, 7L, "u");
|
||||
}
|
||||
|
||||
private void stubAssignment() {
|
||||
|
||||
Reference in New Issue
Block a user