fix(firstleg): validate booking carrier associations
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
package com.baiyue.firstleg.client;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import org.springframework.cloud.openfeign.FeignClient;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestHeader;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@FeignClient(name = "warehouse-service", contextId = "firstlegWarehousePartnerClient",
|
||||
url = "${baiyue.clients.warehouse.url:}", path = "/internal/warehouse-partners")
|
||||
public interface WarehousePartnerClient {
|
||||
@GetMapping("/{tenantId}/{partnerId}/active")
|
||||
ApiResponse<Snapshot> active(@RequestHeader("X-Internal-Service-Token") String token,
|
||||
@PathVariable long tenantId, @PathVariable long partnerId);
|
||||
|
||||
record Snapshot(long id, long tenantId, String partnerName, String partnerType,
|
||||
String carrierCode, List<String> types, String status) {}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestHeader;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import java.util.List;
|
||||
|
||||
@FeignClient(name = "warehouse-service", contextId = "firstlegWarehouseSupplierClient",
|
||||
url = "${baiyue.clients.warehouse.url:}", path = "/internal/warehouse/v1/suppliers")
|
||||
@@ -16,5 +17,11 @@ public interface WarehouseSupplierClient {
|
||||
@RequestParam(defaultValue = "true") boolean requireActive,
|
||||
@RequestParam(required = false) String category);
|
||||
|
||||
record Snapshot(long supplierId, String name, String category, String status) {}
|
||||
@GetMapping
|
||||
ApiResponse<List<Snapshot>> candidates(@RequestHeader("X-Internal-Service-Token") String token,
|
||||
@RequestParam String category,
|
||||
@RequestParam String carrierCode);
|
||||
|
||||
record Snapshot(long supplierId, String name, String category, String status,
|
||||
List<String> carrierCodes) {}
|
||||
}
|
||||
|
||||
+5
@@ -37,6 +37,11 @@ public class PlatformOceanBookingController {
|
||||
@RequestParam long originWarehouseId) {
|
||||
FirstlegScopeGuard.platform(user); return ApiResponse.success(service.channels(originWarehouseId));
|
||||
}
|
||||
@GetMapping("/booking-agents") @PreAuthorize("hasAuthority('platform:firstleg:ocean-booking:list')")
|
||||
public ApiResponse<List<BookingAgentOption>> bookingAgents(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam String carrierCode) {
|
||||
FirstlegScopeGuard.platform(user); return ApiResponse.success(service.bookingAgents(carrierCode));
|
||||
}
|
||||
@GetMapping("/{oceanBookingId}") @PreAuthorize("hasAuthority('platform:firstleg:ocean-booking:list')")
|
||||
public ApiResponse<Detail> detail(@AuthenticationPrincipal AuthenticatedUser user, @PathVariable @Positive long oceanBookingId) { FirstlegScopeGuard.platform(user); return ApiResponse.success(service.detail(oceanBookingId)); }
|
||||
|
||||
|
||||
@@ -100,4 +100,5 @@ public final class OceanBookingDtos {
|
||||
|
||||
public record ChannelOption(Long priceBookVersionId, String channelCode, String channelName,
|
||||
String priceModel, List<String> pricingTaxModes) {}
|
||||
public record BookingAgentOption(Long supplierId, String name, List<String> carrierCodes) {}
|
||||
}
|
||||
|
||||
+74
-9
@@ -6,6 +6,8 @@ import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.firstleg.client.WarehouseMasterDataClient;
|
||||
import com.baiyue.firstleg.client.WarehousePartnerClient;
|
||||
import com.baiyue.firstleg.client.WarehouseSupplierClient;
|
||||
import com.baiyue.firstleg.oceanbooking.domain.OceanBookingStatus;
|
||||
import com.baiyue.firstleg.domain.ContainerSpecificationCatalog;
|
||||
import com.baiyue.firstleg.oceanbooking.dto.OceanBookingDtos.*;
|
||||
@@ -23,6 +25,7 @@ import org.springframework.stereotype.Service;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
import feign.FeignException;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
@@ -44,6 +47,7 @@ public class OceanBookingService {
|
||||
@Resource private ObjectMapper objectMapper;
|
||||
@Resource private WarehouseSupplierSnapshotService suppliers;
|
||||
@Resource private WarehouseMasterDataClient warehouses;
|
||||
@Resource private WarehousePartnerClient partners;
|
||||
@Resource private FirstlegDocumentNumberGenerator documentNumbers;
|
||||
@Value("${baiyue.security.internal-service-token:}") private String internalToken;
|
||||
@Value("${baiyue.warehouse.public-tenant-id:0}") private long publicWarehouseTenantId;
|
||||
@@ -56,6 +60,14 @@ public class OceanBookingService {
|
||||
return repository.page(new Query(query.customerId(), status, trim(query.keyword()), query.page(), query.size()));
|
||||
}
|
||||
|
||||
public List<BookingAgentOption> bookingAgents(String carrierCode) {
|
||||
String normalized = normalizeCarrierCode(carrierCode);
|
||||
return suppliers.bookingCandidates(normalized).stream()
|
||||
.filter(value -> "ACTIVE".equals(value.status()) && "BOOKING".equals(value.category())
|
||||
&& value.carrierCodes() != null && value.carrierCodes().contains(normalized))
|
||||
.map(value -> new BookingAgentOption(value.supplierId(), value.name(), value.carrierCodes())).toList();
|
||||
}
|
||||
|
||||
public PageResult<LinkCandidate> linkCandidates(LinkCandidateQuery query) {
|
||||
return repository.linkCandidates(new LinkCandidateQuery(query.warehouseId(),
|
||||
query.channelCode().trim(), query.containerType().trim(), query.page(), query.size()));
|
||||
@@ -138,6 +150,7 @@ public class OceanBookingService {
|
||||
OceanBookingEntity before = requiredVersion(id, request.expectedVersion());
|
||||
requireStatus(before, OceanBookingStatus.DRAFT);
|
||||
validateRelease(before, repository.parties(id), repository.requirements(id));
|
||||
validateMasterData(before);
|
||||
if (repository.transition(id, OceanBookingStatus.DRAFT.name(), OceanBookingStatus.RELEASED.name(),
|
||||
request.expectedVersion(), operatorId, name) != 1) throw concurrent();
|
||||
OceanBookingEntity after = required(id);
|
||||
@@ -238,27 +251,27 @@ public class OceanBookingService {
|
||||
return value.trim();
|
||||
}
|
||||
|
||||
private record ResolvedDraft(String warehouseName, String channelName, String bookingAgentName) {}
|
||||
private record ResolvedDraft(String warehouseName, String channelName, String shippingCompanyName,
|
||||
String carrierCode, String bookingAgentName) {}
|
||||
private ResolvedDraft resolveDraft(DraftData d) {
|
||||
String warehouseName = null; String channelName = null; String bookingAgentName = trim(d.bookingAgentName());
|
||||
String warehouseName = null; String channelName = null;
|
||||
if (d.warehouseId() != null && StringUtils.hasText(d.channelCode())) {
|
||||
WarehouseMasterDataClient.Warehouse warehouse = requireDomesticTransitWarehouse(d.warehouseId());
|
||||
warehouseName = warehouse.warehouseName();
|
||||
channelName = requireActiveChannel(warehouse.id(), d.channelCode()).channelName();
|
||||
}
|
||||
if (d.bookingAgentId() != null) {
|
||||
var snapshot = suppliers.booking(d.bookingAgentId(), d.bookingAgentName());
|
||||
bookingAgentName = snapshot == null ? null : snapshot.name();
|
||||
} else if (StringUtils.hasText(d.bookingAgentName())) throw badRequest("订舱代理名称不能脱离供应商ID提交");
|
||||
return new ResolvedDraft(warehouseName, channelName, bookingAgentName);
|
||||
var carrier = resolveCarrier(d.shippingCompanyId(), d.shippingCompanyName(), d.carrierCode());
|
||||
var agent = resolveBookingAgent(d.bookingAgentId(), d.bookingAgentName(), carrier == null ? null : carrier.carrierCode());
|
||||
return new ResolvedDraft(warehouseName, channelName, carrier == null ? null : carrier.partnerName(),
|
||||
carrier == null ? null : carrier.carrierCode(), agent == null ? null : agent.name());
|
||||
}
|
||||
private static void copy(OceanBookingEntity v, DraftData d, ResolvedDraft resolved) {
|
||||
v.setCustomerId(d.customerId()); v.setCustomerName(trim(d.customerName()));
|
||||
v.setChannelCode(trim(d.channelCode())); v.setChannelName(resolved.channelName());
|
||||
v.setWarehouseId(d.warehouseId()); v.setWarehouseName(resolved.warehouseName());
|
||||
v.setBillOfLadingNo(trim(d.billOfLadingNo()));
|
||||
v.setShippingCompanyId(d.shippingCompanyId()); v.setShippingCompanyName(trim(d.shippingCompanyName()));
|
||||
v.setCarrierCode(d.carrierCode() == null ? null : d.carrierCode().trim().toUpperCase());
|
||||
v.setShippingCompanyId(d.shippingCompanyId()); v.setShippingCompanyName(resolved.shippingCompanyName());
|
||||
v.setCarrierCode(resolved.carrierCode());
|
||||
v.setBookingAgentId(d.bookingAgentId()); v.setBookingAgentName(resolved.bookingAgentName());
|
||||
v.setVesselName(trim(d.vesselName())); v.setVoyageNo(trim(d.voyageNo()));
|
||||
v.setDeparturePortCode(trim(d.departurePortCode())); v.setDeparturePortName(trim(d.departurePortName()));
|
||||
@@ -268,6 +281,58 @@ public class OceanBookingService {
|
||||
v.setRemark(trim(d.remark()));
|
||||
}
|
||||
|
||||
private void validateMasterData(OceanBookingEntity booking) {
|
||||
resolveCarrier(booking.getShippingCompanyId(), booking.getShippingCompanyName(), booking.getCarrierCode());
|
||||
resolveBookingAgent(booking.getBookingAgentId(), booking.getBookingAgentName(), booking.getCarrierCode());
|
||||
}
|
||||
|
||||
private WarehousePartnerClient.Snapshot resolveCarrier(Long id, String suppliedName, String suppliedCode) {
|
||||
if (id == null) {
|
||||
if (StringUtils.hasText(suppliedName) || StringUtils.hasText(suppliedCode)) throw badRequest("船公司名称和编码不能脱离shippingCompanyId提交");
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
var response = partners.active(internalToken, 0L, id);
|
||||
var value = response == null ? null : response.data();
|
||||
if (response == null || !response.successful() || value == null || value.id() != id
|
||||
|| value.tenantId() != 0L || !"ACTIVE".equals(value.status())
|
||||
|| value.types() == null || !value.types().contains("CARRIER")
|
||||
|| !StringUtils.hasText(value.carrierCode())) throw badRequest("所选船公司不是启用中的CARRIER往来单位");
|
||||
String code = normalizeCarrierCode(value.carrierCode());
|
||||
if (StringUtils.hasText(suppliedCode) && !code.equals(normalizeCarrierCode(suppliedCode))) throw badRequest("船司标准编码与主数据快照不一致");
|
||||
if (StringUtils.hasText(suppliedName) && !value.partnerName().equals(suppliedName.trim())) throw badRequest("船公司名称与主数据快照不一致");
|
||||
return value;
|
||||
} catch (FeignException exception) {
|
||||
throw new BizException(com.baiyue.common.core.api.ResultCode.SERVICE_UNAVAILABLE, "船公司主数据服务暂不可用");
|
||||
}
|
||||
}
|
||||
|
||||
private WarehouseSupplierClient.Snapshot resolveBookingAgent(Long id, String name, String carrierCode) {
|
||||
if (id == null) {
|
||||
if (StringUtils.hasText(name)) throw badRequest("订舱代理名称不能脱离供应商ID提交");
|
||||
return null;
|
||||
}
|
||||
WarehouseSupplierClient.Snapshot snapshot;
|
||||
try {
|
||||
snapshot = suppliers.booking(id, name);
|
||||
} catch (BizException exception) {
|
||||
if (exception.getResultCode() == ResultCode.SERVICE_UNAVAILABLE) throw exception;
|
||||
throw badRequest("所选订舱代理未关联当前船公司");
|
||||
}
|
||||
if (!StringUtils.hasText(carrierCode) || snapshot.carrierCodes() == null
|
||||
|| !snapshot.carrierCodes().contains(normalizeCarrierCode(carrierCode))) {
|
||||
throw badRequest("所选订舱代理未关联当前船公司");
|
||||
}
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
private static String normalizeCarrierCode(String value) {
|
||||
if (!StringUtils.hasText(value)) throw badRequest("carrierCode不能为空");
|
||||
String normalized = value.trim().toUpperCase(java.util.Locale.ROOT);
|
||||
if (!CARRIER_CODES.contains(normalized)) throw badRequest("carrierCode仅支持MATSON、SML、ZIM、COSCO、EMC、HEDE或OTHER");
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private static List<OceanBookingPartyEntity> parties(long id, List<PartyRequest> requests) { if (requests == null) return List.of(); return requests.stream().map(r -> { OceanBookingPartyEntity v = new OceanBookingPartyEntity(); v.setId(IdWorker.getId()); v.setOceanBookingId(id); v.setPartyType(r.partyType().trim().toUpperCase()); v.setPartyName(r.partyName().trim()); v.setAddress(trim(r.address())); v.setContactName(trim(r.contactName())); v.setContactPhone(trim(r.contactPhone())); return v; }).toList(); }
|
||||
private static List<OceanBookingContainerRequirementEntity> requirements(long id, List<ContainerRequirementRequest> requests) { if (requests == null) return List.of(); int[] sort = {0}; return requests.stream().map(r -> { var specification = ContainerSpecificationCatalog.requireMatch(r.containerType(), r.capacityCbm(), r.maxWeightKg()); OceanBookingContainerRequirementEntity v = new OceanBookingContainerRequirementEntity(); v.setId(IdWorker.getId()); v.setOceanBookingId(id); v.setContainerType(specification.containerType()); v.setContainerKind(r.containerKind().trim().toUpperCase()); v.setCapacityCbm(specification.capacityCbm()); v.setMaxWeightKg(specification.maxWeightKg()); v.setQuantity(r.quantity()); v.setSortNo(sort[0]++); return v; }).toList(); }
|
||||
private WarehouseMasterDataClient.Warehouse requireDomesticTransitWarehouse(long warehouseId) {
|
||||
|
||||
+13
@@ -8,6 +8,7 @@ import jakarta.annotation.Resource;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
public class WarehouseSupplierSnapshotService {
|
||||
@@ -18,6 +19,18 @@ public class WarehouseSupplierSnapshotService {
|
||||
return resolve(supplierId, suppliedName, "BOOKING");
|
||||
}
|
||||
|
||||
public List<WarehouseSupplierClient.Snapshot> bookingCandidates(String carrierCode) {
|
||||
try {
|
||||
var response = client.candidates(internalToken, "BOOKING", carrierCode);
|
||||
if (response == null || !response.successful() || response.data() == null) {
|
||||
throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "订舱代理候选服务暂不可用");
|
||||
}
|
||||
return response.data();
|
||||
} catch (FeignException exception) {
|
||||
throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "订舱代理候选服务暂不可用");
|
||||
}
|
||||
}
|
||||
|
||||
public WarehouseSupplierClient.Snapshot trucking(Long supplierId, String suppliedName) {
|
||||
return resolve(supplierId, suppliedName, "TRUCKING");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user