This commit is contained in:
2026-08-14 18:50:07 +08:00
parent 9b36978448
commit 10d3390bb1
17 changed files with 461 additions and 61 deletions
@@ -16,6 +16,7 @@ import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@Validated
@RestController
@@ -26,6 +27,11 @@ public class PlatformOceanBookingController {
@GetMapping @PreAuthorize("hasAuthority('platform:firstleg:ocean-booking:list')")
public ApiResponse<PageResult<OceanBookingEntity>> page(@AuthenticationPrincipal AuthenticatedUser user, @Valid Query query) { FirstlegScopeGuard.platform(user); return ApiResponse.success(service.page(query)); }
@GetMapping("/channels") @PreAuthorize("hasAuthority('platform:firstleg:ocean-booking:list')")
public ApiResponse<List<ChannelOption>> channels(@AuthenticationPrincipal AuthenticatedUser user,
@RequestParam long originWarehouseId) {
FirstlegScopeGuard.platform(user); return ApiResponse.success(service.channels(originWarehouseId));
}
@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)); }
@@ -42,7 +42,6 @@ public final class OceanBookingDtos {
public record DraftData(@NotNull @Positive Long customerId,
@NotBlank @Size(max = 128) String customerName,
@NotBlank @Size(max = 64) String channelCode,
@NotBlank @Size(max = 128) String channelName,
@NotNull @Positive Long warehouseId,
@NotBlank @Size(max = 128) String warehouseName,
@Size(max = 128) String billOfLadingNo,
@@ -92,4 +91,7 @@ public final class OceanBookingDtos {
List<OceanBookingContainerRequirementEntity> containerRequirements,
List<OceanBookingContainerAllocationEntity> containerAllocations,
List<OceanBookingStatusHistoryEntity> histories) {}
public record ChannelOption(Long priceBookVersionId, String channelCode, String channelName,
String priceModel, List<String> pricingTaxModes) {}
}
@@ -10,6 +10,9 @@ 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.repository.PriceBookQueryRepository;
import com.baiyue.firstleg.repository.PriceBookQueryRows.ChannelRow;
import com.baiyue.firstleg.repository.PriceBookQueryRows.ChannelTaxModeRow;
import com.baiyue.firstleg.service.LoadingPickService;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -25,12 +28,14 @@ import java.time.LocalDateTime;
import java.util.HashSet;
import java.util.HexFormat;
import java.util.List;
import java.util.Map;
import java.util.Set;
@Service
public class OceanBookingService {
private static final Set<String> PARTY_TYPES = Set.of("SHIPPER", "CONSIGNEE", "NOTIFY_PARTY");
@Resource private OceanBookingRepository repository;
@Resource private PriceBookQueryRepository priceBooks;
@Resource private ObjectMapper objectMapper;
@Resource private LoadingPickService loadingPickService;
@@ -40,21 +45,36 @@ public class OceanBookingService {
return repository.page(new Query(query.customerId(), status, trim(query.keyword()), query.page(), query.size()));
}
public List<ChannelOption> channels(long originWarehouseId) {
if (originWarehouseId <= 0) throw badRequest("请选择有效起运仓");
Long versionId = priceBooks.activeVersionId();
if (versionId == null) return List.of();
List<ChannelRow> rows = priceBooks.channels(versionId, originWarehouseId);
Map<String, List<String>> taxModes = priceBooks.channelTaxModes(versionId, originWarehouseId).stream()
.filter(row -> row != null && StringUtils.hasText(row.getChannelCode())
&& StringUtils.hasText(row.getPricingTaxMode()))
.collect(java.util.stream.Collectors.groupingBy(row -> row.getChannelCode().trim(),
java.util.stream.Collectors.mapping(ChannelTaxModeRow::getPricingTaxMode,
java.util.stream.Collectors.toList())));
return rows.stream().map(row -> channelOption(row, taxModes)).toList();
}
public Detail detail(long oceanBookingId) { return detail(required(oceanBookingId)); }
@Transactional
public Detail create(long operatorId, String operatorName, String key, CreateRequest request) {
if (request.expectedVersion() != 0) throw concurrent();
validateDraft(request.data());
validateDraftStructure(request.data());
String action = "CREATE";
String hash = hash(action, null, request);
Detail replay = claimOrReplay(null, operatorId, action, key, hash);
if (replay != null) return replay;
ChannelOption channel = requireActiveChannel(request.data().warehouseId(), request.data().channelCode());
OceanBookingEntity value = new OceanBookingEntity();
value.setId(IdWorker.getId());
value.setBookingNo("OB" + value.getId());
value.setStatus(OceanBookingStatus.DRAFT.name());
copy(value, request.data());
copy(value, request.data(), channel.channelName());
value.setCreatedBy(operatorId); value.setCreatedByName(operatorName);
value.setUpdatedBy(operatorId); value.setUpdatedByName(operatorName); value.setVersion(0);
repository.insert(value);
@@ -67,13 +87,14 @@ public class OceanBookingService {
@Transactional
public Detail updateDraft(long oceanBookingId, long operatorId, String operatorName, String key,
UpdateDraftRequest request) {
validateDraft(request.data());
validateDraftStructure(request.data());
String action = "UPDATE_DRAFT";
Detail replay = claimOrReplay(oceanBookingId, operatorId, action, key, hash(action, oceanBookingId, request));
if (replay != null) return replay;
OceanBookingEntity before = requiredVersion(oceanBookingId, request.expectedVersion());
requireStatus(before, OceanBookingStatus.DRAFT);
OceanBookingEntity value = new OceanBookingEntity(); value.setId(oceanBookingId); copy(value, request.data());
ChannelOption channel = requireActiveChannel(request.data().warehouseId(), request.data().channelCode());
OceanBookingEntity value = new OceanBookingEntity(); value.setId(oceanBookingId); copy(value, request.data(), channel.channelName());
if (repository.updateDraft(value, request.expectedVersion(), operatorId, operatorName) != 1) throw concurrent();
repository.replaceParties(oceanBookingId, parties(oceanBookingId, request.data().parties()));
repository.replaceRequirements(oceanBookingId, requirements(oceanBookingId, request.data().containerRequirements()));
@@ -90,6 +111,7 @@ public class OceanBookingService {
if (replay != null) return replay;
OceanBookingEntity before = requiredVersion(id, request.expectedVersion());
requireStatus(before, OceanBookingStatus.SUBMITTED);
requireActiveChannel(before.getWarehouseId(), before.getChannelCode());
validateConfirmation(before, repository.requirements(id));
if (repository.transition(id, OceanBookingStatus.SUBMITTED.name(), OceanBookingStatus.CONFIRMED.name(),
null, null, request.expectedVersion(), operatorId, name) != 1) throw concurrent();
@@ -263,7 +285,7 @@ public class OceanBookingService {
private OceanBookingEntity requiredVersion(long id, int version) { OceanBookingEntity value = required(id); if (!Integer.valueOf(version).equals(value.getVersion())) throw concurrent(); return value; }
private static void requireStatus(OceanBookingEntity value, OceanBookingStatus status) { if (!status.name().equals(value.getStatus())) throw conflict("订舱状态不允许当前操作"); }
private void validateDraft(DraftData data) {
private void validateDraftStructure(DraftData data) {
if (data.estimatedSailingAt() != null || data.estimatedArrivalAt() != null
|| StringUtils.hasText(data.billOfLadingNo()) || StringUtils.hasText(data.vesselName())
|| StringUtils.hasText(data.voyageNo())) {
@@ -284,9 +306,31 @@ public class OceanBookingService {
}
}
private static void validateRange(LocalDateTime from, LocalDateTime to) { if (from != null && to != null && to.isBefore(from)) throw badRequest("预计到港时间不能早于预计开船时间"); }
private static void copy(OceanBookingEntity v, DraftData d) {
private ChannelOption requireActiveChannel(Long originWarehouseId, String channelCode) {
if (originWarehouseId == null || originWarehouseId <= 0) throw badRequest("请选择有效起运仓");
String normalized = normalizeChannel(channelCode);
Long versionId = priceBooks.activeVersionId();
if (versionId == null) throw conflict("当前没有 ACTIVE 价格版本");
ChannelRow row = priceBooks.channels(versionId, originWarehouseId).stream()
.filter(value -> normalized.equals(normalizeChannel(value.getChannelCode())))
.findFirst().orElseThrow(() -> conflict("所选物流渠道已失效,请重新选择"));
if (!StringUtils.hasText(row.getChannelName())) throw conflict("所选物流渠道名称配置无效");
return channelOption(row, java.util.Map.of());
}
private static ChannelOption channelOption(ChannelRow row, java.util.Map<String, List<String>> taxModes) {
return new ChannelOption(row.getPriceBookVersionId(), row.getChannelCode(), row.getChannelName(),
row.getPriceModel(), List.copyOf(taxModes.getOrDefault(row.getChannelCode().trim(), List.of())));
}
private static String normalizeChannel(String value) {
if (!StringUtils.hasText(value)) throw badRequest("channelCode不能为空");
return value.trim();
}
private static void copy(OceanBookingEntity v, DraftData d, String resolvedChannelName) {
v.setCustomerId(d.customerId()); v.setCustomerName(d.customerName().trim());
v.setChannelCode(d.channelCode().trim()); v.setChannelName(d.channelName().trim());
v.setChannelCode(d.channelCode().trim()); v.setChannelName(resolvedChannelName);
v.setWarehouseId(d.warehouseId()); v.setWarehouseName(d.warehouseName().trim());
v.setBillOfLadingNo(null);
v.setShippingCompanyId(d.shippingCompanyId()); v.setShippingCompanyName(trim(d.shippingCompanyName()));