fix: bind overseas receiving to shared warehouse

This commit is contained in:
2026-08-24 21:46:19 +08:00
parent 3607bebbfd
commit 8cc4c6f6da
61 changed files with 774 additions and 118 deletions
@@ -24,6 +24,8 @@ public class LastlegOrder {
private String sourceOrderNo;
private String rootOrderId;
private String customerName;
private String sourceWarehouseId;
private String sourceWarehouseName;
private String overseasWarehouseId;
private String overseasWarehouseName;
private String fbaWarehouseCode;
@@ -17,6 +17,7 @@ public class InboundPlan {
private String customerId;
private Long sourceOrderId;
private String containerNo;
private Long overseasWarehouseId;
private String status;
private Integer lineCount;
private Integer palletCount;
@@ -7,6 +7,7 @@ import com.baiyue.lastleg.inboundplan.entity.InboundPlan;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;
import java.util.List;
@@ -47,4 +48,10 @@ public interface InboundPlanMapper extends BaseMapper<InboundPlan> {
@Select("SELECT * FROM ll_inbound_plan WHERE customer_id=#{customerId} AND source_order_id=#{sourceOrderId} LIMIT 1")
InboundPlan findBySourceOrder(@Param("customerId") String customerId, @Param("sourceOrderId") Long sourceOrderId);
@Update("UPDATE ll_inbound_plan SET overseas_warehouse_id=#{warehouseId},updated_at=NOW(3) " +
"WHERE id=#{planId} AND customer_id=#{customerId} " +
"AND (overseas_warehouse_id IS NULL OR overseas_warehouse_id=#{warehouseId})")
int claimReceivingWarehouse(@Param("customerId") String customerId, @Param("planId") long planId,
@Param("warehouseId") long warehouseId);
}
@@ -38,7 +38,7 @@ public class ReceivingContainerController {
public ApiResponse<List<ReceivingContainerResponses.Candidate>> candidates(
@AuthenticationPrincipal AuthenticatedUser user) {
Identity identity = identity(user);
return ApiResponse.success(service.candidates(identity.operatorKey()));
return ApiResponse.success(service.candidates(identity.operatorKey(), identity.overseasWarehouseId()));
}
@GetMapping("/{containerNo}")
@@ -46,21 +46,21 @@ public class ReceivingContainerController {
public ApiResponse<ReceivingContainerResponses.Session> detail(@AuthenticationPrincipal AuthenticatedUser user,
@PathVariable String containerNo) {
Identity identity = identity(user);
return ApiResponse.success(service.detail(identity.operatorKey(), containerNo));
return ApiResponse.success(service.detail(identity.operatorKey(), identity.overseasWarehouseId(), containerNo));
}
@PostMapping("/{containerNo}/prepare")
public ApiResponse<ReceivingContainerResponses.Session> prepare(@AuthenticationPrincipal AuthenticatedUser user,
@PathVariable String containerNo, @Valid @RequestBody ReceivingContainerRequests.Action request) {
Identity identity = identity(user);
return ApiResponse.success(service.prepare(identity.operatorKey(), identity.operatorName(),
return ApiResponse.success(service.prepare(identity.operatorKey(), identity.operatorName(), identity.overseasWarehouseId(),
containerNo, request));
}
@PostMapping(value = "/current/unload-photos/files", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ApiResponse<ReceivingContainerResponses.Photo> uploadPhoto(@AuthenticationPrincipal AuthenticatedUser user,
@RequestPart("file") MultipartFile file) {
Identity identity = identity(user);
Identity identity = currentIdentity(user);
if (user.userId() == null) throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "PDA user ID is required");
return ApiResponse.success(service.uploadPhoto(identity.operatorKey(), user.userId(), file));
}
@@ -68,13 +68,13 @@ public class ReceivingContainerController {
@PostMapping("/current/sign")
public ApiResponse<ReceivingContainerResponses.Session> sign(@AuthenticationPrincipal AuthenticatedUser user,
@Valid @RequestBody ReceivingContainerRequests.Sign request) {
Identity identity = identity(user);
Identity identity = currentIdentity(user);
return ApiResponse.success(service.sign(identity.operatorKey(), identity.operatorName(), request));
}
@GetMapping("/current")
public ApiResponse<ReceivingContainerResponses.Session> current(@AuthenticationPrincipal AuthenticatedUser user) {
Identity identity = identity(user);
Identity identity = currentIdentity(user);
return ApiResponse.success(service.current(identity.operatorKey()));
}
@@ -82,7 +82,7 @@ public class ReceivingContainerController {
public ApiResponse<ReceivingContainerResponses.CartonResolved> resolveCarton(
@AuthenticationPrincipal AuthenticatedUser user,
@Valid @RequestBody ReceivingContainerRequests.CartonResolve request) {
Identity identity = identity(user);
Identity identity = currentIdentity(user);
return ApiResponse.success(service.resolveCarton(identity.operatorKey(), request));
}
@@ -90,7 +90,7 @@ public class ReceivingContainerController {
public ApiResponse<ReceivingContainerResponses.SkuResolved> resolveSku(
@AuthenticationPrincipal AuthenticatedUser user,
@Valid @RequestBody ReceivingContainerRequests.SkuResolve request) {
Identity identity = identity(user);
Identity identity = currentIdentity(user);
return ApiResponse.success(service.resolveSku(identity.operatorKey(), request));
}
@@ -98,28 +98,28 @@ public class ReceivingContainerController {
public ApiResponse<ReceivingContainerResponses.PalletResolved> resolvePallet(
@AuthenticationPrincipal AuthenticatedUser user,
@Valid @RequestBody ReceivingContainerRequests.PalletResolve request) {
Identity identity = identity(user);
Identity identity = currentIdentity(user);
return ApiResponse.success(service.resolvePallet(identity.operatorKey(), request));
}
@PostMapping("/current/receipts/bulk")
public ApiResponse<ReceivingContainerResponses.Receipt> receiveBulk(@AuthenticationPrincipal AuthenticatedUser user,
@Valid @RequestBody ReceivingContainerRequests.BulkReceive request) {
Identity identity = identity(user);
Identity identity = currentIdentity(user);
return ApiResponse.success(service.receiveBulk(identity.operatorKey(), identity.operatorName(), request));
}
@PostMapping("/current/receipts/carton")
public ApiResponse<ReceivingContainerResponses.Receipt> receiveCarton(@AuthenticationPrincipal AuthenticatedUser user,
@Valid @RequestBody ReceivingContainerRequests.CartonReceive request) {
Identity identity = identity(user);
Identity identity = currentIdentity(user);
return ApiResponse.success(service.receiveCarton(identity.operatorKey(), identity.operatorName(), request));
}
@PostMapping("/current/complete")
public ApiResponse<ReceivingContainerResponses.Session> complete(@AuthenticationPrincipal AuthenticatedUser user,
@Valid @RequestBody ReceivingContainerRequests.Action request) {
Identity identity = identity(user);
Identity identity = currentIdentity(user);
return ApiResponse.success(service.complete(identity.operatorKey(), request));
}
@@ -131,10 +131,20 @@ public class ReceivingContainerController {
if (user.permissions() == null || !user.permissions().contains(OPERATE_PERMISSION)) {
throw new ResponseStatusException(HttpStatus.FORBIDDEN, OPERATE_PERMISSION + " permission is required");
}
if (user.overseasWarehouseId() == null || user.overseasWarehouseId() <= 0) {
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "PDA operator must be bound to one overseas warehouse");
}
String operator = String.valueOf(user.userId());
return new Identity(operator,
user.username() == null || user.username().isBlank() ? operator : user.username());
user.username() == null || user.username().isBlank() ? operator : user.username(),
user.overseasWarehouseId());
}
private record Identity(String operatorKey, String operatorName) { }
private Identity currentIdentity(AuthenticatedUser user) {
Identity identity = identity(user);
service.requireCurrentWarehouse(identity.operatorKey(), identity.overseasWarehouseId());
return identity;
}
private record Identity(String operatorKey, String operatorName, long overseasWarehouseId) { }
}
@@ -10,7 +10,8 @@ public final class ReceivingContainerResponses {
List<String> destinationCodes, int plannedPalletCount,
int totalPackages, int receivedPackages, boolean currentAccount) { }
public record Session(String containerNo, String status, String operatorName, int version,
public record Session(String containerNo, Long overseasWarehouseId, String overseasWarehouseName,
String status, String operatorName, int version,
List<SignedPhoto> photos, LocalDateTime preparedAt, LocalDateTime signedAt,
LocalDateTime receivingAt, LocalDateTime completedAt,
int totalPackages, int receivedPackages, int pendingPackages,
@@ -18,19 +19,23 @@ public final class ReceivingContainerResponses {
public record SignedPhoto(ReceivingPhotoRole role, long fileId) { }
public record Inbound(String inboundCode, String destinationType, String destinationWarehouseId,
public record Inbound(String inboundCode, String destinationType,
String overseasWarehouseId, String overseasWarehouseName,
String destinationWarehouseId,
String destinationWarehouseCode, String destinationWarehouseName,
String fbaWarehouseCode, int plannedPalletCount, int totalPackages,
int receivedPackages, int pendingPackages) { }
public record CartonResolved(String cartonMark, String skuCode, String inboundCode,
String destinationType, String destinationWarehouseId,
String destinationType, String overseasWarehouseId,
String overseasWarehouseName, String destinationWarehouseId,
String destinationWarehouseCode, String destinationWarehouseName,
String fbaWarehouseCode, int plannedPalletCount, int totalPackages,
int receivedPackages, int pendingPackages) { }
public record SkuResolved(String skuCode, String productName, String inboundCode,
String destinationType, String destinationWarehouseId,
String destinationType, String overseasWarehouseId,
String overseasWarehouseName, String destinationWarehouseId,
String destinationWarehouseCode, String destinationWarehouseName,
String fbaWarehouseCode, int plannedPalletCount, int totalPackages,
int receivedPackages, int pendingPackages) { }
@@ -15,6 +15,8 @@ public class ReceivingContainerSession {
private Long id;
private String customerId;
private String containerNo;
private Long overseasWarehouseId;
private String overseasWarehouseName;
private String operatorKey;
private String operatorName;
private String activeOperatorKey;
@@ -4,12 +4,14 @@ import jakarta.annotation.PostConstruct;
import jakarta.annotation.Resource;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.client.MultipartBodyBuilder;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestClient;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.server.ResponseStatusException;
import java.io.IOException;
@@ -42,10 +44,14 @@ public class HttpUnloadPhotoFileGateway implements UnloadPhotoFileGateway {
.header("X-Internal-Service-Token", internalToken).header("X-Operator-Id", String.valueOf(operatorId))
.contentType(MediaType.MULTIPART_FORM_DATA).body(body.build()).retrieve()
.onStatus(HttpStatusCode::isError, (request, result) -> {
throw new IllegalArgumentException("Unload photo upload failed");
throw new ResponseStatusException(result.getStatusCode().is4xxClientError()
? HttpStatus.BAD_REQUEST : HttpStatus.SERVICE_UNAVAILABLE,
"Unload photo upload failed");
}).body(MetadataEnvelope.class);
requireSuccess(response, "Unload photo upload failed");
Metadata data = response == null ? null : response.data();
if (data == null || data.fileId() == null) throw new IllegalArgumentException("File service returned no photo metadata");
if (data == null || data.fileId() == null) throw new ResponseStatusException(
HttpStatus.SERVICE_UNAVAILABLE, "File service returned no photo metadata");
return new UploadedFile(data.fileId(), data.fileName(), data.contentType(),
data.sizeBytes() == null ? 0 : data.sizeBytes());
}
@@ -58,22 +64,40 @@ public class HttpUnloadPhotoFileGateway implements UnloadPhotoFileGateway {
tenantId, BUSINESS_TYPE, taskId, fileId)
.header("X-Internal-Service-Token", internalToken)
.retrieve().onStatus(HttpStatusCode::isError, (request, result) -> {
throw new IllegalArgumentException("Unload photo does not exist or its ownership is invalid");
throw new ResponseStatusException(result.getStatusCode().is4xxClientError()
? HttpStatus.BAD_REQUEST : HttpStatus.SERVICE_UNAVAILABLE,
"Unload photo does not exist or its ownership is invalid");
}).body(MetadataEnvelope.class);
requireSuccess(response, "Unload photo does not exist or its ownership is invalid");
Metadata data = response == null ? null : response.data();
if (data == null || data.fileId() == null || data.fileId() != fileId
|| data.tenantId() == null || data.tenantId() != tenantId
|| !BUSINESS_TYPE.equals(data.businessType()) || !String.valueOf(taskId).equals(data.businessId())) {
throw new IllegalArgumentException("Unload photo metadata does not match the PDA task");
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
"Unload photo metadata does not match the PDA task");
}
}
private long tenantId(String customerId) {
try { return Long.parseLong(customerId); }
catch (NumberFormatException exception) { throw new IllegalArgumentException("Customer ID must be numeric", exception); }
catch (NumberFormatException exception) { throw new ResponseStatusException(
HttpStatus.BAD_REQUEST, "Customer ID must be numeric", exception); }
}
private record MetadataEnvelope(Metadata data) {}
private static void requireSuccess(MetadataEnvelope response, String fallback) {
if (response == null) throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, fallback);
if ("200".equals(response.code())) return;
HttpStatus status = switch (String.valueOf(response.code())) {
case "A0400", "A0422" -> HttpStatus.BAD_REQUEST;
case "A0403" -> HttpStatus.FORBIDDEN;
case "A0404" -> HttpStatus.NOT_FOUND;
default -> HttpStatus.SERVICE_UNAVAILABLE;
};
throw new ResponseStatusException(status,
response.message() == null || response.message().isBlank() ? fallback : response.message());
}
private record MetadataEnvelope(String code, String message, Metadata data) {}
private record Metadata(Long fileId, Long tenantId, String businessType, String businessId,
String fileName, String contentType, Long sizeBytes) {}
}
@@ -2,8 +2,10 @@ package com.baiyue.lastleg.inboundplan.pda.gateway;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestClient;
import org.springframework.web.server.ResponseStatusException;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.Resource;
@@ -111,9 +113,37 @@ public class HttpWarehouseResourceGateway implements WarehouseResourceGateway {
response.data().fbaWarehouseCode(), response.data().version());
}
@Override
public WarehouseReference requireActiveSharedWarehouse(long warehouseId) {
WarehouseReferenceResponse response = client.get()
.uri("/internal/v1/warehouse-resource-locks/shared-warehouses/{warehouseId}", warehouseId)
.header("X-Internal-Service-Token", internalToken)
.retrieve().onStatus(HttpStatusCode::isError, (request, result) -> {
throw new ResponseStatusException(result.getStatusCode().value() == 404
? HttpStatus.NOT_FOUND : (result.getStatusCode().is4xxClientError()
? HttpStatus.BAD_REQUEST : HttpStatus.SERVICE_UNAVAILABLE),
"Shared overseas warehouse lookup failed");
}).body(WarehouseReferenceResponse.class);
if (response == null) {
throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE,
"Shared overseas warehouse service returned no response");
}
if ("A0404".equals(response.code())) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND,
"Active shared overseas warehouse was not found");
}
if (!"200".equals(response.code()) || response.data() == null
|| response.data().warehouseId() != warehouseId || !"ACTIVE".equals(response.data().status())) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
"Invalid shared overseas warehouse binding");
}
return response.data();
}
private record WarehousePalletResponse(WarehousePalletData data) {}
private record WarehousePalletData(long id, String palletCode, long warehouseId, long locationId,
String status, String fbaWarehouseCode, int version) {}
private record ReserveEnvelope(ReserveData data) {}
private record ReserveData(long lockId) {}
private record WarehouseReferenceResponse(String code, String message, WarehouseReference data) {}
}
@@ -8,6 +8,9 @@ public interface WarehouseResourceGateway {
void scrap(String customerId, long resourceLockId);
void scrapPallet(String customerId, long palletId);
WarehousePalletReference resolvePallet(String palletCode);
default WarehouseReference requireActiveSharedWarehouse(long warehouseId) {
throw new UnsupportedOperationException("Shared warehouse resolution is not configured");
}
record WarehousePalletReference(long palletId, String palletNo, long warehouseId, long locationId,
String status, String fbaWarehouseCode, int version) {
@@ -15,4 +18,7 @@ public interface WarehouseResourceGateway {
this(palletId, palletNo, 0L, locationId, null, null, 0);
}
}
record WarehouseReference(long warehouseId, String warehouseCode, String warehouseName,
String status) { }
}
@@ -80,8 +80,9 @@ public class ReceivingContainerService {
.configure(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS, true);
}
public List<ReceivingContainerResponses.Candidate> candidates(String operatorKey) {
public List<ReceivingContainerResponses.Candidate> candidates(String operatorKey, long overseasWarehouseId) {
ReceivingContainerSession current = sessions.findActiveByOperator(activeOperatorKey(operatorKey));
requireSessionWarehouse(current, overseasWarehouseId);
Map<String, List<InboundPlan>> grouped = new LinkedHashMap<>();
plans.selectList(new LambdaQueryWrapper<InboundPlan>()
.ne(InboundPlan::getStatus, "COMPLETED").orderByAsc(InboundPlan::getCreatedAt)).forEach(plan -> {
@@ -89,7 +90,7 @@ public class ReceivingContainerService {
grouped.computeIfAbsent(plan.getContainerNo(), ignored -> new ArrayList<>()).add(plan);
}
});
return grouped.entrySet().stream().map(entry -> {
return grouped.entrySet().stream().filter(entry -> eligible(entry.getValue(), overseasWarehouseId)).map(entry -> {
requireSingleCustomer(entry.getValue(), entry.getKey());
Totals totals = totals(entry.getValue());
boolean active = current != null && entry.getKey().equals(current.getContainerNo());
@@ -99,27 +100,39 @@ public class ReceivingContainerService {
}).toList();
}
public ReceivingContainerResponses.Session detail(String operatorKey, String containerNo) {
public ReceivingContainerResponses.Session detail(String operatorKey, long overseasWarehouseId, String containerNo) {
List<InboundPlan> containerPlans = requirePlans(containerNo);
String customerId = requireSingleCustomer(containerPlans, containerNo);
if (!eligible(containerPlans, overseasWarehouseId)) throw notFound("Container is assigned to another overseas warehouse");
ReceivingContainerSession current = sessions.findActiveByOperator(activeOperatorKey(operatorKey));
requireSessionWarehouse(current, overseasWarehouseId);
if (current != null && !containerNo.equals(current.getContainerNo())) {
throw conflict("Current account is locked to another selected container");
}
WarehouseResourceGateway.WarehouseReference receivingWarehouse =
warehouse.requireActiveSharedWarehouse(overseasWarehouseId);
return snapshot(current != null && containerNo.equals(current.getContainerNo()) ? current : waitingView(
customerId, operatorKey, containerNo), containerPlans);
customerId, operatorKey, receivingWarehouse, containerNo), containerPlans);
}
@Transactional
public ReceivingContainerResponses.Session prepare(String operatorKey, String operatorName, String containerNo,
public ReceivingContainerResponses.Session prepare(String operatorKey, String operatorName, long overseasWarehouseId, String containerNo,
ReceivingContainerRequests.Action request) {
PrepareCommand command = new PrepareCommand(containerNo, request.idempotencyKey());
ReceivingContainerOperation replay = replay(operatorKey, "PREPARE", request.idempotencyKey(), command);
if (replay != null) return read(replay, ReceivingContainerResponses.Session.class);
if (replay != null) {
ReceivingContainerResponses.Session result = read(replay, ReceivingContainerResponses.Session.class);
if (!Objects.equals(result.overseasWarehouseId(), overseasWarehouseId)) {
throw conflict("Prepared container belongs to another overseas warehouse");
}
return result;
}
List<InboundPlan> containerPlans = requirePlans(containerNo);
String customerId = requireSingleCustomer(containerPlans, containerNo);
if (!eligible(containerPlans, overseasWarehouseId)) throw conflict("Container is assigned to another overseas warehouse");
String activeOperatorKey = activeOperatorKey(operatorKey);
ReceivingContainerSession active = sessions.lockActiveByOperator(activeOperatorKey);
requireSessionWarehouse(active, overseasWarehouseId);
if (active != null) {
if (!containerNo.equals(active.getContainerNo())) throw conflict("Finish the current container before selecting another one");
ReceivingContainerResponses.Session result = snapshot(active, containerPlans);
@@ -128,9 +141,14 @@ public class ReceivingContainerService {
if (sessions.lockActiveByContainer(activeContainerKey(containerNo)) != null) {
throw conflict("Container is already signed or prepared by another account");
}
WarehouseResourceGateway.WarehouseReference receivingWarehouse =
warehouse.requireActiveSharedWarehouse(overseasWarehouseId);
claimReceivingWarehouse(customerId, containerPlans, receivingWarehouse);
LocalDateTime now = LocalDateTime.now();
ReceivingContainerSession session = new ReceivingContainerSession();
session.setId(IdWorker.getId()); session.setCustomerId(customerId); session.setContainerNo(containerNo);
session.setOverseasWarehouseId(overseasWarehouseId);
session.setOverseasWarehouseName(receivingWarehouse.warehouseName());
session.setOperatorKey(operatorKey); session.setOperatorName(operatorName);
session.setActiveOperatorKey(activeOperatorKey); session.setActiveContainerKey(activeContainerKey(containerNo));
session.setStatus("WAIT_SIGN"); session.setPreparedAt(now); session.setCreatedAt(now); session.setUpdatedAt(now);
@@ -175,6 +193,10 @@ public class ReceivingContainerService {
return snapshot(session, requirePlans(session.getCustomerId(), session.getContainerNo()));
}
public void requireCurrentWarehouse(String operatorKey, long overseasWarehouseId) {
requireSessionWarehouse(sessions.findActiveByOperator(activeOperatorKey(operatorKey)), overseasWarehouseId);
}
public ReceivingContainerResponses.CartonResolved resolveCarton(String operatorKey,
ReceivingContainerRequests.CartonResolve request) {
ReceivingContainerSession session = requireSigned(operatorKey);
@@ -190,7 +212,8 @@ public class ReceivingContainerService {
}
ReceivingContainerResponses.Inbound inbound = inbound(ref);
return new ReceivingContainerResponses.CartonResolved(carton.getCartonMark(), carton.getSkuCode(),
inbound.inboundCode(), inbound.destinationType(), inbound.destinationWarehouseId(),
inbound.inboundCode(), inbound.destinationType(), inbound.overseasWarehouseId(),
inbound.overseasWarehouseName(), inbound.destinationWarehouseId(),
inbound.destinationWarehouseCode(), inbound.destinationWarehouseName(), inbound.fbaWarehouseCode(),
inbound.plannedPalletCount(), inbound.totalPackages(), inbound.receivedPackages(), inbound.pendingPackages());
}
@@ -403,7 +426,8 @@ public class ReceivingContainerService {
long actualWarehouse;
try { actualWarehouse = Long.parseLong(ref.order().getOverseasWarehouseId()); }
catch (Exception exception) { throw conflict("Inbound order has no valid receiving warehouse ID"); }
if (pallet.warehouseId() != actualWarehouse) throw conflict("Pallet belongs to another overseas warehouse");
long assignedWarehouse = ref.plan().getOverseasWarehouseId() == null ? actualWarehouse : ref.plan().getOverseasWarehouseId();
if (pallet.warehouseId() != assignedWarehouse) throw conflict("Pallet belongs to another overseas warehouse");
String type = destinationType(ref.line(), ref.order());
if ("FBA".equals(type)) {
if (ref.line().getFbaWarehouseCode() == null || ref.line().getFbaWarehouseCode().isBlank()
@@ -438,7 +462,8 @@ public class ReceivingContainerService {
.map(this::inbound).toList();
int total = inbounds.stream().mapToInt(ReceivingContainerResponses.Inbound::totalPackages).sum();
int received = inbounds.stream().mapToInt(ReceivingContainerResponses.Inbound::receivedPackages).sum();
return new ReceivingContainerResponses.Session(session.getContainerNo(), session.getStatus(), session.getOperatorName(),
return new ReceivingContainerResponses.Session(session.getContainerNo(), session.getOverseasWarehouseId(),
session.getOverseasWarehouseName(), session.getStatus(), session.getOperatorName(),
value(session.getVersion()), signPhotos(session), session.getPreparedAt(), session.getSignedAt(),
session.getReceivingAt(), session.getCompletedAt(), total, received, Math.max(0, total - received), inbounds);
}
@@ -451,6 +476,8 @@ public class ReceivingContainerService {
.mapToInt(value -> value(value.getPickedPackageCount())).sum();
int total = value(ref.line().getRequiredPackageCount());
return new ReceivingContainerResponses.Inbound(ref.line().getInboundCode(), destinationType(ref.line(), ref.order()),
ref.plan().getOverseasWarehouseId() == null ? ref.order().getOverseasWarehouseId()
: String.valueOf(ref.plan().getOverseasWarehouseId()), ref.order().getOverseasWarehouseName(),
ref.line().getDestinationWarehouseId(), ref.line().getDestinationWarehouseCode(),
ref.line().getDestinationWarehouseName(), ref.line().getFbaWarehouseCode(),
value(ref.line().getPlannedPalletCount()), total, received, Math.max(0, total - received));
@@ -517,7 +544,8 @@ public class ReceivingContainerService {
ReceivingContainerResponses.Inbound inbound = inbound(sku.ref());
int received = receivedSkuPackages(sku.ref().order().getCustomerId(), sku.ref().line().getId(), sku.skuCode());
return new ReceivingContainerResponses.SkuResolved(sku.skuCode(), sku.productName(), inbound.inboundCode(),
inbound.destinationType(), inbound.destinationWarehouseId(), inbound.destinationWarehouseCode(),
inbound.destinationType(), inbound.overseasWarehouseId(), inbound.overseasWarehouseName(),
inbound.destinationWarehouseId(), inbound.destinationWarehouseCode(),
inbound.destinationWarehouseName(), inbound.fbaWarehouseCode(), inbound.plannedPalletCount(),
sku.expectedPackages(), received, Math.max(0, sku.expectedPackages() - received));
}
@@ -600,12 +628,56 @@ public class ReceivingContainerService {
if (sessions.updateById(session) != 1) throw conflict("Receiving container session changed concurrently");
}
private ReceivingContainerSession waitingView(String customerId, String operatorKey, String containerNo) {
private ReceivingContainerSession waitingView(String customerId, String operatorKey,
WarehouseResourceGateway.WarehouseReference warehouse,
String containerNo) {
ReceivingContainerSession value = new ReceivingContainerSession(); value.setCustomerId(customerId);
value.setOverseasWarehouseId(warehouse.warehouseId());
value.setOverseasWarehouseName(warehouse.warehouseName());
value.setOperatorKey(operatorKey); value.setContainerNo(containerNo); value.setStatus("WAIT_SIGN"); value.setVersion(0);
return value;
}
private boolean eligible(List<InboundPlan> containerPlans, long overseasWarehouseId) {
Set<Long> assigned = containerPlans.stream().map(InboundPlan::getOverseasWarehouseId)
.filter(Objects::nonNull).collect(Collectors.toSet());
if (assigned.size() > 1 || (!assigned.isEmpty() && containerPlans.stream()
.anyMatch(plan -> plan.getOverseasWarehouseId() == null))) {
throw conflict("Container plans have inconsistent receiving warehouse assignments");
}
return assigned.isEmpty() || assigned.contains(overseasWarehouseId);
}
private void claimReceivingWarehouse(String customerId, List<InboundPlan> containerPlans,
WarehouseResourceGateway.WarehouseReference warehouse) {
long overseasWarehouseId = warehouse.warehouseId();
String warehouseId = String.valueOf(overseasWarehouseId);
for (InboundPlan plan : containerPlans) {
int claimed = plans.claimReceivingWarehouse(customerId, plan.getId(), overseasWarehouseId);
InboundPlan actual = plans.selectById(plan.getId());
if ((claimed == 0 && actual == null) || actual == null
|| !Objects.equals(actual.getOverseasWarehouseId(), overseasWarehouseId)) {
throw conflict("Container was claimed by another overseas warehouse");
}
plan.setOverseasWarehouseId(overseasWarehouseId);
for (InboundPlanLine line : lines.selectList(new LambdaQueryWrapper<InboundPlanLine>()
.eq(InboundPlanLine::getCustomerId, customerId).eq(InboundPlanLine::getPlanId, plan.getId()))) {
orders.claimReceivingWarehouse(customerId, line.getOrderId(), warehouseId,
warehouse.warehouseName());
LastlegOrder order = orders.selectById(line.getOrderId());
if (order == null || !warehouseId.equals(order.getOverseasWarehouseId())) {
throw conflict("Inbound order was claimed by another overseas warehouse");
}
}
}
}
private void requireSessionWarehouse(ReceivingContainerSession session, long overseasWarehouseId) {
if (session != null && !Objects.equals(session.getOverseasWarehouseId(), overseasWarehouseId)) {
throw conflict("Current container belongs to another overseas warehouse");
}
}
private ReceivingContainerOperation replay(String operatorKey, String action, String key, Object request) {
key = idempotencyKey(key);
ReceivingContainerOperation value = operations.find(operatorKey, key);
@@ -22,6 +22,7 @@ import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ResponseStatusException;
@@ -40,9 +41,9 @@ public class InventoryQueryController {
@Operation(summary = "Get inbound-order package inventory totals", description = "Required authority: " + OverseasPermissions.INVENTORY_LIST)
public ApiResponse<InventoryDashboardResponse> summary(
@AuthenticationPrincipal AuthenticatedUser user,
@RequestParam(required = false) String customerId,
@Valid InventoryQueryRequests.BalanceQuery query) {
requirePlatform(user);
return ApiResponse.success(service.dashboard(null, query));
return ApiResponse.success(service.dashboard(platformScope(user, customerId), query));
}
@GetMapping(BASE_PATH + "/balances")
@@ -50,9 +51,9 @@ public class InventoryQueryController {
@Operation(summary = "Page inventory by customer, warehouse and inbound order", description = "Required authority: " + OverseasPermissions.INVENTORY_LIST)
public ApiResponse<PageResult<InventoryBalanceView>> balances(
@AuthenticationPrincipal AuthenticatedUser user,
@RequestParam(required = false) String customerId,
@Valid InventoryQueryRequests.BalanceQuery query) {
requirePlatform(user);
return ApiResponse.success(service.balances(null, query));
return ApiResponse.success(service.balances(platformScope(user, customerId), query));
}
@GetMapping(BASE_PATH + "/balances/{balanceId}/positions")
@@ -61,9 +62,9 @@ public class InventoryQueryController {
public ApiResponse<List<InventoryPositionView>> positions(
@AuthenticationPrincipal AuthenticatedUser user,
@PathVariable long balanceId,
@RequestParam(required = false) String customerId,
@Valid InventoryQueryRequests.PositionQuery query) {
requirePlatform(user);
return ApiResponse.success(service.positions(null, balanceId, query));
return ApiResponse.success(service.positions(platformScope(user, customerId), balanceId, query));
}
@GetMapping(BASE_PATH + "/ledger")
@@ -71,9 +72,9 @@ public class InventoryQueryController {
@Operation(summary = "Page append-only inventory movements", description = "Required authority: " + OverseasPermissions.INVENTORY_LIST)
public ApiResponse<PageResult<InventoryLedgerView>> ledgers(
@AuthenticationPrincipal AuthenticatedUser user,
@RequestParam(required = false) String customerId,
@Valid InventoryQueryRequests.LedgerQuery query) {
requirePlatform(user);
return ApiResponse.success(service.ledgers(null, query));
return ApiResponse.success(service.ledgers(platformScope(user, customerId), query));
}
@GetMapping(value = BASE_PATH + "/balances/export", produces = "text/csv")
@@ -81,9 +82,9 @@ public class InventoryQueryController {
@Operation(summary = "Export filtered inbound-order inventory as UTF-8 CSV", description = "Required authority: " + OverseasPermissions.INVENTORY_EXPORT)
public ResponseEntity<byte[]> exportBalances(
@AuthenticationPrincipal AuthenticatedUser user,
@RequestParam(required = false) String customerId,
@Valid InventoryQueryRequests.BalanceQuery query) {
requirePlatform(user);
return csv(service.exportBalances(null, query), "inventory-balances.csv");
return csv(service.exportBalances(platformScope(user, customerId), query), "inventory-balances.csv");
}
@GetMapping(value = BASE_PATH + "/ledger/export", produces = "text/csv")
@@ -91,9 +92,9 @@ public class InventoryQueryController {
@Operation(summary = "Export filtered inventory movements as UTF-8 CSV", description = "Required authority: " + OverseasPermissions.INVENTORY_EXPORT)
public ResponseEntity<byte[]> exportLedgers(
@AuthenticationPrincipal AuthenticatedUser user,
@RequestParam(required = false) String customerId,
@Valid InventoryQueryRequests.LedgerQuery query) {
requirePlatform(user);
return csv(service.exportLedgers(null, query), "inventory-ledger.csv");
return csv(service.exportLedgers(platformScope(user, customerId), query), "inventory-ledger.csv");
}
private static ResponseEntity<byte[]> csv(byte[] body, String filename) {
@@ -103,12 +104,14 @@ public class InventoryQueryController {
.body(body);
}
private void requirePlatform(AuthenticatedUser user) {
private String platformScope(AuthenticatedUser user, String requestedCustomerId) {
if (user == null || user.tenantId() == null) {
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
}
if (!"PLATFORM".equals(user.appCode()) || user.tenantId() != 0L) {
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Platform application required");
}
return requestedCustomerId == null || requestedCustomerId.isBlank() || "0".equals(requestedCustomerId.trim())
? null : requestedCustomerId.trim();
}
}
@@ -89,8 +89,6 @@ public interface InventoryQueryMapper {
@Select("<script>SELECT * FROM ll_inventory_position WHERE customer_id=#{customerId} AND balance_id=#{balanceId}" +
"<if test='q.palletNo != null and q.palletNo != \"\"'> AND pallet_no LIKE CONCAT('%',#{q.palletNo},'%')</if>" +
"<if test='q.batchNo != null and q.batchNo != \"\"'> AND batch_no LIKE CONCAT('%',#{q.batchNo},'%')</if>" +
"<if test='q.skuCode != null and q.skuCode != \"\"'> AND sku_code LIKE CONCAT('%',#{q.skuCode},'%')</if>" +
"<if test='q.warehouseLocationCode != null and q.warehouseLocationCode != \"\"'> AND warehouse_location_code LIKE CONCAT('%',#{q.warehouseLocationCode},'%')</if>" +
"<choose>" +
"<when test='q.inventoryStatus == \"AVAILABLE\"'> AND (available_packages+allocated_packages+picked_packages) &gt; 0</when>" +
"<when test='q.inventoryStatus == \"HOLD\"'> AND hold_packages &gt; 0</when>" +
@@ -35,4 +35,11 @@ public interface LastlegOrderMapper extends BaseMapper<LastlegOrder> {
"AND shipped_package_count+#{packages} <= package_count")
int transferPickedToShipped(@Param("customerId") String customerId, @Param("id") Long id,
@Param("packages") long packages, @Param("outboundNo") String outboundNo);
@Update("UPDATE ll_order SET overseas_warehouse_id=#{warehouseId},overseas_warehouse_name=#{warehouseName}," +
"updated_at=NOW(3) WHERE id=#{orderId} AND customer_id=#{customerId} " +
"AND (overseas_warehouse_id IS NULL OR overseas_warehouse_id=#{warehouseId})")
int claimReceivingWarehouse(@Param("customerId") String customerId, @Param("orderId") long orderId,
@Param("warehouseId") String warehouseId,
@Param("warehouseName") String warehouseName);
}
@@ -170,7 +170,8 @@ public class LoadingCompletedConsumer {
order.setInboundCode(source.inboundCode()); order.setSourceRegion(event.sourceRegion().name());
order.setSourceInboundId(source.sourceInboundId()); order.setRootInboundId(source.rootInboundId());
order.setSourceOrderId(source.sourceOrderId()); order.setSourceOrderNo(source.sourceOrderNo()); order.setRootOrderId(source.rootOrderId());
order.setOverseasWarehouseId(payload.warehouseId()); order.setOverseasWarehouseName(payload.warehouseName());
order.setSourceWarehouseId(payload.warehouseId()); order.setSourceWarehouseName(payload.warehouseName());
order.setOverseasWarehouseId(null); order.setOverseasWarehouseName(null);
order.setFbaWarehouseCode(source.destinationWarehouseCode()); order.setContainerNo(payload.containerNo());
order.setContainerStatus("待转柜"); order.setAbnormal(false); order.setDetained(false);
order.setPackageCount(source.packageCount()); order.setPalletCount(dropshipPallets.size());